(listener);
503 | }
504 |
505 | @Override
506 | public boolean deliverSelfNotifications() {
507 | return true;
508 | }
509 |
510 | @Override
511 | public void onChange(boolean selfChange, Uri uri) {
512 | RemotePreferencePath path = mUriParser.parse(uri);
513 |
514 | // We use a weak reference to mimic the behavior of SharedPreferences.
515 | // The code which registered the listener is responsible for holding a
516 | // reference to it. If at any point we find that the listener has been
517 | // garbage collected, we unregister the observer.
518 | OnSharedPreferenceChangeListener listener = mListener.get();
519 | if (listener == null) {
520 | mContext.getContentResolver().unregisterContentObserver(this);
521 | } else {
522 | listener.onSharedPreferenceChanged(RemotePreferences.this, path.key);
523 | }
524 | }
525 | }
526 | }
527 |
--------------------------------------------------------------------------------
/library/src/main/java/com/crossbowffs/remotepreferences/RemotePreferenceProvider.java:
--------------------------------------------------------------------------------
1 | package com.crossbowffs.remotepreferences;
2 |
3 | import android.content.ContentProvider;
4 | import android.content.ContentResolver;
5 | import android.content.ContentValues;
6 | import android.content.Context;
7 | import android.content.SharedPreferences;
8 | import android.database.ContentObserver;
9 | import android.database.Cursor;
10 | import android.database.MatrixCursor;
11 | import android.net.Uri;
12 | import android.os.Build;
13 |
14 | import java.util.HashMap;
15 | import java.util.Map;
16 |
17 | /**
18 | *
19 | * Exposes {@link SharedPreferences} to other apps running on the device.
20 | *
21 | *
22 | *
23 | * You must extend this class and declare a 0-argument constructor which
24 | * calls the super constructor with the appropriate authority and
25 | * preference file name parameters. Remember to add your provider to
26 | * your {@code AndroidManifest.xml} file and set the {@code android:exported}
27 | * property to true.
28 | *
29 | *
30 | *
31 | * For granular access control, override {@link #checkAccess(String, String, boolean)}
32 | * and return {@code false} to deny the operation.
33 | *
34 | *
35 | *
36 | * To access the data from a remote process, use {@link RemotePreferences}
37 | * initialized with the same authority and the desired preference file name.
38 | * You may also manually query the provider; here are some example queries
39 | * and their equivalent {@link SharedPreferences} API calls:
40 | *
41 | *
42 | *
43 | * query(uri = content://authority/foo/bar)
44 | * = getSharedPreferences("foo").get("bar")
45 | *
46 | * query(uri = content://authority/foo)
47 | * = getSharedPreferences("foo").getAll()
48 | *
49 | * insert(uri = content://authority/foo/bar, values = [{type = TYPE_STRING, value = "baz"}])
50 | * = getSharedPreferences("foo").edit().putString("bar", "baz").commit()
51 | *
52 | * insert(uri = content://authority/foo, values = [{key = "bar", type = TYPE_STRING, value = "baz"}])
53 | * = getSharedPreferences("foo").edit().putString("bar", "baz").commit()
54 | *
55 | * delete(uri = content://authority/foo/bar)
56 | * = getSharedPreferences("foo").edit().remove("bar").commit()
57 | *
58 | * delete(uri = content://authority/foo)
59 | * = getSharedPreferences("foo").edit().clear().commit()
60 | *
61 | *
62 | *
63 | * Also note that if you are querying string sets, they will be returned
64 | * in a serialized form: {@code ["foo;bar", "baz"]} is converted to
65 | * {@code "foo\\;bar;baz;"} (note the trailing semicolon). Booleans are
66 | * converted into integers: 1 for true, 0 for false. This is only applicable
67 | * if you are using raw queries; all of these subtleties are transparently
68 | * handled by {@link RemotePreferences}.
69 | *
70 | */
71 | public abstract class RemotePreferenceProvider extends ContentProvider implements SharedPreferences.OnSharedPreferenceChangeListener {
72 | private final Uri mBaseUri;
73 | private final RemotePreferenceFile[] mPrefFiles;
74 | private final Map mPreferences;
75 | private final RemotePreferenceUriParser mUriParser;
76 |
77 | /**
78 | * Initializes the remote preference provider with the specified
79 | * authority and preference file names. The authority must match the
80 | * {@code android:authorities} property defined in your manifest
81 | * file. Only the specified preference files will be accessible
82 | * through the provider. This constructor assumes all preferences
83 | * are located in credential protected storage; if you are using
84 | * device protected storage, use
85 | * {@link #RemotePreferenceProvider(String, RemotePreferenceFile[])}.
86 | *
87 | * @param authority The authority of the provider.
88 | * @param prefFileNames The names of the preference files to expose.
89 | */
90 | public RemotePreferenceProvider(String authority, String[] prefFileNames) {
91 | this(authority, RemotePreferenceFile.fromFileNames(prefFileNames));
92 | }
93 |
94 | /**
95 | * Initializes the remote preference provider with the specified
96 | * authority and preference files. The authority must match the
97 | * {@code android:authorities} property defined in your manifest
98 | * file. Only the specified preference files will be accessible
99 | * through the provider.
100 | *
101 | * @param authority The authority of the provider.
102 | * @param prefFiles The preference files to expose.
103 | */
104 | public RemotePreferenceProvider(String authority, RemotePreferenceFile[] prefFiles) {
105 | mBaseUri = Uri.parse("content://" + authority);
106 | mPrefFiles = prefFiles;
107 | mPreferences = new HashMap(prefFiles.length);
108 | mUriParser = new RemotePreferenceUriParser(authority);
109 | }
110 |
111 | /**
112 | * Checks whether the specified preference is accessible by callers.
113 | * The default implementation returns {@code true} for all accesses.
114 | * You may override this method to control which preferences can be
115 | * read or written. Note that {@code prefKey} will be {@code ""} when
116 | * accessing an entire file, so a whitelist is strongly recommended
117 | * over a blacklist (your default case should be {@code return false},
118 | * not {@code return true}).
119 | *
120 | * @param prefFileName The name of the preference file.
121 | * @param prefKey The preference key. This is an empty string when handling the
122 | * {@link SharedPreferences#getAll()} and
123 | * {@link SharedPreferences.Editor#clear()} operations.
124 | * @param write {@code true} for put/remove/clear operations; {@code false} for get operations.
125 | * @return {@code true} if the access is allowed; {@code false} otherwise.
126 | */
127 | protected boolean checkAccess(String prefFileName, String prefKey, boolean write) {
128 | return true;
129 | }
130 |
131 | /**
132 | * Called at application startup to register preference change listeners.
133 | *
134 | * @return Always returns {@code true}.
135 | */
136 | @Override
137 | public boolean onCreate() {
138 | // We register the shared preference listeners whenever the provider
139 | // is created. This method is called before almost all other code in
140 | // the app, which ensures that we never miss a preference change.
141 | for (RemotePreferenceFile file : mPrefFiles) {
142 | Context context = getContext();
143 | if (file.isDeviceProtected() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
144 | context = context.createDeviceProtectedStorageContext();
145 | }
146 | SharedPreferences prefs = getSharedPreferences(context, file.getFileName());
147 | prefs.registerOnSharedPreferenceChangeListener(this);
148 | mPreferences.put(file.getFileName(), prefs);
149 | }
150 | return true;
151 | }
152 |
153 | /**
154 | * Generate {@link SharedPreferences} to store the key-value data.
155 | * Override this method to provide a custom implementation of {@link SharedPreferences}.
156 | *
157 | * @param context The context that should be used to get the preferences object.
158 | * @param prefFileName The name of the preference file.
159 | * @return An object implementing the {@link SharedPreferences} interface.
160 | */
161 | protected SharedPreferences getSharedPreferences(Context context, String prefFileName) {
162 | return context.getSharedPreferences(prefFileName, Context.MODE_PRIVATE);
163 | }
164 |
165 | /**
166 | * Returns a cursor for the specified preference(s). If {@code uri}
167 | * is in the form {@code content://authority/prefFileName/prefKey}, the
168 | * cursor will contain a single row containing the queried preference.
169 | * If {@code uri} is in the form {@code content://authority/prefFileName},
170 | * the cursor will contain one row for each preference in the specified
171 | * file.
172 | *
173 | * @param uri Specifies the preference file and key (optional) to query.
174 | * @param projection Specifies which fields should be returned in the cursor.
175 | * @param selection Ignored.
176 | * @param selectionArgs Ignored.
177 | * @param sortOrder Ignored.
178 | * @return A cursor used to access the queried preference data.
179 | */
180 | @Override
181 | public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
182 | RemotePreferencePath prefPath = mUriParser.parse(uri);
183 |
184 | SharedPreferences prefs = getSharedPreferencesOrThrow(prefPath, false);
185 | Map prefMap = prefs.getAll();
186 |
187 | // If no projection is specified, we return all columns.
188 | if (projection == null) {
189 | projection = RemoteContract.COLUMN_ALL;
190 | }
191 |
192 | // Fill out the cursor with the preference data. If the caller
193 | // didn't ask for a particular preference, we return all of them.
194 | MatrixCursor cursor = new MatrixCursor(projection);
195 | if (isSingleKey(prefPath.key)) {
196 | Object prefValue = prefMap.get(prefPath.key);
197 | cursor.addRow(buildRow(projection, prefPath.key, prefValue));
198 | } else {
199 | for (Map.Entry entry : prefMap.entrySet()) {
200 | String prefKey = entry.getKey();
201 | Object prefValue = entry.getValue();
202 | cursor.addRow(buildRow(projection, prefKey, prefValue));
203 | }
204 | }
205 |
206 | return cursor;
207 | }
208 |
209 | /**
210 | * Not used in RemotePreferences. Always returns {@code null}.
211 | *
212 | * @param uri Ignored.
213 | * @return Always returns {@code null}.
214 | */
215 | @Override
216 | public String getType(Uri uri) {
217 | return null;
218 | }
219 |
220 | /**
221 | * Writes the value of the specified preference(s). If no key is specified,
222 | * {@link RemoteContract#COLUMN_TYPE} must be equal to {@link RemoteContract#TYPE_NULL},
223 | * representing the {@link SharedPreferences.Editor#clear()} operation.
224 | *
225 | * @param uri Specifies the preference file and key (optional) to write.
226 | * @param values Specifies the key (optional), type and value of the preference to write.
227 | * @return A URI representing the preference written, or {@code null} on failure.
228 | */
229 | @Override
230 | public Uri insert(Uri uri, ContentValues values) {
231 | if (values == null) {
232 | return null;
233 | }
234 |
235 | RemotePreferencePath prefPath = mUriParser.parse(uri);
236 | String prefKey = getKeyFromUriOrValues(prefPath, values);
237 |
238 | SharedPreferences prefs = getSharedPreferencesOrThrow(prefPath, true);
239 | SharedPreferences.Editor editor = prefs.edit();
240 |
241 | putPreference(editor, prefKey, values);
242 |
243 | if (editor.commit()) {
244 | return getPreferenceUri(prefPath.fileName, prefKey);
245 | } else {
246 | return null;
247 | }
248 | }
249 |
250 | /**
251 | * Writes multiple preference values at once. {@code uri} must
252 | * be in the form {@code content://authority/prefFileName}. See
253 | * {@link #insert(Uri, ContentValues)} for more information.
254 | *
255 | * @param uri Specifies the preference file to write to.
256 | * @param values See {@link #insert(Uri, ContentValues)}.
257 | * @return The number of preferences written, or 0 on failure.
258 | */
259 | @Override
260 | public int bulkInsert(Uri uri, ContentValues[] values) {
261 | RemotePreferencePath prefPath = mUriParser.parse(uri);
262 |
263 | if (isSingleKey(prefPath.key)) {
264 | throw new IllegalArgumentException("Cannot bulk insert with single key URI");
265 | }
266 |
267 | SharedPreferences prefs = getSharedPreferencesByName(prefPath.fileName);
268 | SharedPreferences.Editor editor = prefs.edit();
269 |
270 | for (ContentValues value : values) {
271 | String prefKey = getKeyFromValues(value);
272 | checkAccessOrThrow(prefPath.withKey(prefKey), true);
273 | putPreference(editor, prefKey, value);
274 | }
275 |
276 | if (editor.commit()) {
277 | return values.length;
278 | } else {
279 | return 0;
280 | }
281 | }
282 |
283 | /**
284 | * Deletes the specified preference(s). If {@code uri} is in the form
285 | * {@code content://authority/prefFileName/prefKey}, this will only delete
286 | * the one preference specified in the URI; if {@code uri} is in the form
287 | * {@code content://authority/prefFileName}, clears all preferences.
288 | *
289 | * @param uri Specifies the preference file and key (optional) to delete.
290 | * @param selection Ignored.
291 | * @param selectionArgs Ignored.
292 | * @return 1 if the preferences committed successfully, or 0 on failure.
293 | */
294 | @Override
295 | public int delete(Uri uri, String selection, String[] selectionArgs) {
296 | RemotePreferencePath prefPath = mUriParser.parse(uri);
297 |
298 | SharedPreferences prefs = getSharedPreferencesOrThrow(prefPath, true);
299 | SharedPreferences.Editor editor = prefs.edit();
300 |
301 | if (isSingleKey(prefPath.key)) {
302 | editor.remove(prefPath.key);
303 | } else {
304 | editor.clear();
305 | }
306 |
307 | // There's no reliable method of getting the actual number of
308 | // preference values changed, so callers should not rely on this
309 | // value. A return value of 1 means success, 0 means failure.
310 | if (editor.commit()) {
311 | return 1;
312 | } else {
313 | return 0;
314 | }
315 | }
316 |
317 | /**
318 | * Updates the value of the specified preference(s). This is a wrapper
319 | * around {@link #insert(Uri, ContentValues)} if {@code values} is not
320 | * {@code null}, or {@link #delete(Uri, String, String[])} if {@code values}
321 | * is {@code null}.
322 | *
323 | * @param uri Specifies the preference file and key (optional) to update.
324 | * @param values {@code null} to delete the preference,
325 | * @param selection Ignored.
326 | * @param selectionArgs Ignored.
327 | * @return 1 if the preferences committed successfully, or 0 on failure.
328 | */
329 | @Override
330 | public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
331 | if (values == null) {
332 | return delete(uri, selection, selectionArgs);
333 | } else {
334 | return insert(uri, values) != null ? 1 : 0;
335 | }
336 | }
337 |
338 | /**
339 | * Listener for preference value changes in the local application.
340 | * Re-raises the event through the
341 | * {@link ContentResolver#notifyChange(Uri, ContentObserver)} API
342 | * to any registered {@link ContentObserver} objects. Note that this
343 | * is NOT called for {@link SharedPreferences.Editor#clear()}.
344 | *
345 | * @param prefs The preference file that changed.
346 | * @param prefKey The preference key that changed.
347 | */
348 | @Override
349 | public void onSharedPreferenceChanged(SharedPreferences prefs, String prefKey) {
350 | RemotePreferenceFile prefFile = getSharedPreferencesFile(prefs);
351 | Uri uri = getPreferenceUri(prefFile.getFileName(), prefKey);
352 | Context context = getContext();
353 | if (prefFile.isDeviceProtected() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
354 | context = context.createDeviceProtectedStorageContext();
355 | }
356 | ContentResolver resolver = context.getContentResolver();
357 | resolver.notifyChange(uri, null);
358 | }
359 |
360 | /**
361 | * Writes the value of the specified preference(s). If {@code prefKey}
362 | * is empty, {@code values} must contain {@link RemoteContract#TYPE_NULL}
363 | * for the type, representing the {@link SharedPreferences.Editor#clear()}
364 | * operation.
365 | *
366 | * @param editor The preference file to modify.
367 | * @param prefKey The preference key to modify, or {@code null} for the entire file.
368 | * @param values The values to write.
369 | */
370 | private void putPreference(SharedPreferences.Editor editor, String prefKey, ContentValues values) {
371 | // Get the new value type. Note that we manually check
372 | // for null, then unbox the Integer so we don't cause a NPE.
373 | Integer type = values.getAsInteger(RemoteContract.COLUMN_TYPE);
374 | if (type == null) {
375 | throw new IllegalArgumentException("Invalid or no preference type specified");
376 | }
377 |
378 | // deserializeInput makes sure the actual object type matches
379 | // the expected type, so we must perform this step before actually
380 | // performing any actions.
381 | Object rawValue = values.get(RemoteContract.COLUMN_VALUE);
382 | Object value = RemoteUtils.deserializeInput(rawValue, type);
383 |
384 | // If we are writing to the "directory" and the type is null,
385 | // then we should clear the preferences.
386 | if (!isSingleKey(prefKey)) {
387 | if (type == RemoteContract.TYPE_NULL) {
388 | editor.clear();
389 | return;
390 | } else {
391 | throw new IllegalArgumentException("Attempting to insert preference with null or empty key");
392 | }
393 | }
394 |
395 | switch (type) {
396 | case RemoteContract.TYPE_NULL:
397 | editor.remove(prefKey);
398 | break;
399 | case RemoteContract.TYPE_STRING:
400 | editor.putString(prefKey, (String)value);
401 | break;
402 | case RemoteContract.TYPE_STRING_SET:
403 | if (Build.VERSION.SDK_INT >= 11) {
404 | editor.putStringSet(prefKey, RemoteUtils.castStringSet(value));
405 | } else {
406 | throw new IllegalArgumentException("String set preferences not supported on API < 11");
407 | }
408 | break;
409 | case RemoteContract.TYPE_INT:
410 | editor.putInt(prefKey, (Integer)value);
411 | break;
412 | case RemoteContract.TYPE_LONG:
413 | editor.putLong(prefKey, (Long)value);
414 | break;
415 | case RemoteContract.TYPE_FLOAT:
416 | editor.putFloat(prefKey, (Float)value);
417 | break;
418 | case RemoteContract.TYPE_BOOLEAN:
419 | editor.putBoolean(prefKey, (Boolean)value);
420 | break;
421 | default:
422 | throw new IllegalArgumentException("Cannot set preference with type " + type);
423 | }
424 | }
425 |
426 | /**
427 | * Used to project a preference value to the schema requested by the caller.
428 | *
429 | * @param projection The projection requested by the caller.
430 | * @param key The preference key.
431 | * @param value The preference value.
432 | * @return A row representing the preference using the given schema.
433 | */
434 | private Object[] buildRow(String[] projection, String key, Object value) {
435 | Object[] row = new Object[projection.length];
436 | for (int i = 0; i < row.length; ++i) {
437 | String col = projection[i];
438 | if (RemoteContract.COLUMN_KEY.equals(col)) {
439 | row[i] = key;
440 | } else if (RemoteContract.COLUMN_TYPE.equals(col)) {
441 | row[i] = RemoteUtils.getPreferenceType(value);
442 | } else if (RemoteContract.COLUMN_VALUE.equals(col)) {
443 | row[i] = RemoteUtils.serializeOutput(value);
444 | } else {
445 | throw new IllegalArgumentException("Invalid column name: " + col);
446 | }
447 | }
448 | return row;
449 | }
450 |
451 | /**
452 | * Returns whether the specified key represents a single preference key
453 | * (as opposed to the entire preference file).
454 | *
455 | * @param prefKey The preference key to check.
456 | * @return Whether the key refers to a single preference.
457 | */
458 | private static boolean isSingleKey(String prefKey) {
459 | return prefKey != null;
460 | }
461 |
462 | /**
463 | * Parses the preference key from {@code values}. If the key is not
464 | * specified in the values, {@code null} is returned.
465 | *
466 | * @param values The query values to parse.
467 | * @return The parsed key, or {@code null} if no key was found.
468 | */
469 | private static String getKeyFromValues(ContentValues values) {
470 | String key = values.getAsString(RemoteContract.COLUMN_KEY);
471 | if (key != null && key.length() == 0) {
472 | key = null;
473 | }
474 | return key;
475 | }
476 |
477 | /**
478 | * Parses the preference key from the specified sources. Since there
479 | * are two ways to specify the key (from the URI or from the query values),
480 | * the only allowed combinations are:
481 | *
482 | * uri.key == values.key
483 | * uri.key != null and values.key == null = URI key is used
484 | * uri.key == null and values.key != null = values key is used
485 | * uri.key == null and values.key == null = no key
486 | *
487 | * If none of these conditions are met, an exception is thrown.
488 | *
489 | * @param prefPath Parsed URI key from {@code mUriParser.parse(uri)}.
490 | * @param values Query values provided by the caller.
491 | * @return The parsed key, or {@code null} if the key refers to a preference file.
492 | */
493 | private static String getKeyFromUriOrValues(RemotePreferencePath prefPath, ContentValues values) {
494 | String uriKey = prefPath.key;
495 | String valuesKey = getKeyFromValues(values);
496 | if (isSingleKey(uriKey) && isSingleKey(valuesKey)) {
497 | // If a key is specified in both the URI and
498 | // ContentValues, they must match
499 | if (!uriKey.equals(valuesKey)) {
500 | throw new IllegalArgumentException("Conflicting keys specified in URI and ContentValues");
501 | }
502 | return uriKey;
503 | } else if (isSingleKey(uriKey)) {
504 | return uriKey;
505 | } else if (isSingleKey(valuesKey)) {
506 | return valuesKey;
507 | } else {
508 | return null;
509 | }
510 | }
511 |
512 | /**
513 | * Checks that the caller has permissions to access the specified preference.
514 | * Throws an exception if permission is denied.
515 | *
516 | * @param prefPath The preference file and key to be accessed.
517 | * @param write Whether the operation will modify the preference.
518 | */
519 | private void checkAccessOrThrow(RemotePreferencePath prefPath, boolean write) {
520 | // For backwards compatibility, checkAccess takes an empty string when
521 | // referring to the whole file.
522 | String prefKey = prefPath.key;
523 | if (!isSingleKey(prefKey)) {
524 | prefKey = "";
525 | }
526 |
527 | if (!checkAccess(prefPath.fileName, prefKey, write)) {
528 | throw new SecurityException("Insufficient permissions to access: " + prefPath);
529 | }
530 | }
531 |
532 | /**
533 | * Returns the {@link SharedPreferences} instance with the specified name.
534 | * This is essentially equivalent to {@link Context#getSharedPreferences(String, int)},
535 | * except that it will used the internally cached version, and throws an
536 | * exception if the provider was not configured to access that preference file.
537 | *
538 | * @param prefFileName The name of the preference file to access.
539 | * @return The {@link SharedPreferences} instance with the specified file name.
540 | */
541 | private SharedPreferences getSharedPreferencesByName(String prefFileName) {
542 | SharedPreferences prefs = mPreferences.get(prefFileName);
543 | if (prefs == null) {
544 | throw new IllegalArgumentException("Unknown preference file name: " + prefFileName);
545 | }
546 | return prefs;
547 | }
548 |
549 | /**
550 | * Returns the file name for a {@link SharedPreferences} instance.
551 | * Throws an exception if the provider was not configured to access
552 | * the specified preferences.
553 | *
554 | * @param prefs The shared preferences object.
555 | * @return The name of the preference file.
556 | */
557 | private String getSharedPreferencesFileName(SharedPreferences prefs) {
558 | for (Map.Entry entry : mPreferences.entrySet()) {
559 | if (entry.getValue() == prefs) {
560 | return entry.getKey();
561 | }
562 | }
563 | throw new IllegalArgumentException("Unknown preference file");
564 | }
565 |
566 | /**
567 | * Get the corresponding {@link RemotePreferenceFile} object for a
568 | * {@link SharedPreferences} instance. Throws an exception if the
569 | * provider was not configured to access the specified preferences.
570 | *
571 | * @param prefs The shared preferences object.
572 | * @return The corresponding {@link RemotePreferenceFile} object.
573 | */
574 | private RemotePreferenceFile getSharedPreferencesFile(SharedPreferences prefs) {
575 | String prefFileName = getSharedPreferencesFileName(prefs);
576 | for (RemotePreferenceFile file : mPrefFiles) {
577 | if (file.getFileName().equals(prefFileName)) {
578 | return file;
579 | }
580 | }
581 | throw new IllegalArgumentException("Unknown preference file");
582 | }
583 |
584 | /**
585 | * Returns the {@link SharedPreferences} instance with the specified name,
586 | * checking that the caller has permissions to access the specified key within
587 | * that file. If not, an exception will be thrown.
588 | *
589 | * @param prefPath The preference file and key to be accessed.
590 | * @param write Whether the operation will modify the preference.
591 | * @return The {@link SharedPreferences} instance with the specified file name.
592 | */
593 | private SharedPreferences getSharedPreferencesOrThrow(RemotePreferencePath prefPath, boolean write) {
594 | checkAccessOrThrow(prefPath, write);
595 | return getSharedPreferencesByName(prefPath.fileName);
596 | }
597 |
598 | /**
599 | * Builds a URI for the specified preference file and key that can be used
600 | * to later query the same preference.
601 | *
602 | * @param prefFileName The preference file.
603 | * @param prefKey The preference key.
604 | * @return A URI representing the specified preference.
605 | */
606 | private Uri getPreferenceUri(String prefFileName, String prefKey) {
607 | Uri.Builder builder = mBaseUri.buildUpon().appendPath(prefFileName);
608 | if (isSingleKey(prefKey)) {
609 | builder.appendPath(prefKey);
610 | }
611 | return builder.build();
612 | }
613 | }
614 |
--------------------------------------------------------------------------------