SettingsProvider.java revision 67f175cf07ff52c5bee30536caad2e97800ab5b8
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.providers.settings;
18
19import java.io.FileNotFoundException;
20import java.security.SecureRandom;
21import java.util.HashMap;
22import java.util.HashSet;
23import java.util.List;
24import java.util.Map;
25import java.util.concurrent.atomic.AtomicBoolean;
26import java.util.concurrent.atomic.AtomicInteger;
27
28import android.app.ActivityManager;
29import android.app.AppOpsManager;
30import android.app.backup.BackupManager;
31import android.content.BroadcastReceiver;
32import android.content.ContentProvider;
33import android.content.ContentUris;
34import android.content.ContentValues;
35import android.content.Context;
36import android.content.Intent;
37import android.content.IntentFilter;
38import android.content.pm.PackageManager;
39import android.content.pm.UserInfo;
40import android.content.res.AssetFileDescriptor;
41import android.database.AbstractCursor;
42import android.database.Cursor;
43import android.database.sqlite.SQLiteDatabase;
44import android.database.sqlite.SQLiteException;
45import android.database.sqlite.SQLiteQueryBuilder;
46import android.media.RingtoneManager;
47import android.net.Uri;
48import android.os.Binder;
49import android.os.Bundle;
50import android.os.DropBoxManager;
51import android.os.FileObserver;
52import android.os.ParcelFileDescriptor;
53import android.os.Process;
54import android.os.SystemProperties;
55import android.os.UserHandle;
56import android.os.UserManager;
57import android.provider.MediaStore;
58import android.provider.Settings;
59import android.provider.Settings.Secure;
60import android.text.TextUtils;
61import android.util.Log;
62import android.util.LruCache;
63import android.util.Slog;
64import android.util.SparseArray;
65
66public class SettingsProvider extends ContentProvider {
67    private static final String TAG = "SettingsProvider";
68    private static final boolean LOCAL_LOGV = false;
69
70    private static final boolean USER_CHECK_THROWS = true;
71
72    private static final String TABLE_SYSTEM = "system";
73    private static final String TABLE_SECURE = "secure";
74    private static final String TABLE_GLOBAL = "global";
75    private static final String TABLE_FAVORITES = "favorites";
76    private static final String TABLE_OLD_FAVORITES = "old_favorites";
77
78    private static final String[] COLUMN_VALUE = new String[] { "value" };
79
80    // Caches for each user's settings, access-ordered for acting as LRU.
81    // Guarded by themselves.
82    private static final int MAX_CACHE_ENTRIES = 200;
83    private static final SparseArray<SettingsCache> sSystemCaches
84            = new SparseArray<SettingsCache>();
85    private static final SparseArray<SettingsCache> sSecureCaches
86            = new SparseArray<SettingsCache>();
87    private static final SettingsCache sGlobalCache = new SettingsCache(TABLE_GLOBAL);
88
89    // The count of how many known (handled by SettingsProvider)
90    // database mutations are currently being handled for this user.
91    // Used by file observers to not reload the database when it's ourselves
92    // modifying it.
93    private static final SparseArray<AtomicInteger> sKnownMutationsInFlight
94            = new SparseArray<AtomicInteger>();
95
96    // Each defined user has their own settings
97    protected final SparseArray<DatabaseHelper> mOpenHelpers = new SparseArray<DatabaseHelper>();
98
99    // Keep the list of managed profiles synced here
100    private List<UserInfo> mManagedProfiles = null;
101
102    // Over this size we don't reject loading or saving settings but
103    // we do consider them broken/malicious and don't keep them in
104    // memory at least:
105    private static final int MAX_CACHE_ENTRY_SIZE = 500;
106
107    private static final Bundle NULL_SETTING = Bundle.forPair("value", null);
108
109    // Used as a sentinel value in an instance equality test when we
110    // want to cache the existence of a key, but not store its value.
111    private static final Bundle TOO_LARGE_TO_CACHE_MARKER = Bundle.forPair("_dummy", null);
112
113    private UserManager mUserManager;
114    private BackupManager mBackupManager;
115
116    /**
117     * Settings which need to be treated as global/shared in multi-user environments.
118     */
119    static final HashSet<String> sSecureGlobalKeys;
120    static final HashSet<String> sSystemGlobalKeys;
121
122    // Settings that cannot be modified if associated user restrictions are enabled.
123    static final Map<String, String> sRestrictedKeys;
124
125    private static final String DROPBOX_TAG_USERLOG = "restricted_profile_ssaid";
126
127    static final HashSet<String> sSecureCloneToManagedKeys;
128    static final HashSet<String> sSystemCloneToManagedKeys;
129
130    static {
131        // Keys (name column) from the 'secure' table that are now in the owner user's 'global'
132        // table, shared across all users
133        // These must match Settings.Secure.MOVED_TO_GLOBAL
134        sSecureGlobalKeys = new HashSet<String>();
135        Settings.Secure.getMovedKeys(sSecureGlobalKeys);
136
137        // Keys from the 'system' table now moved to 'global'
138        // These must match Settings.System.MOVED_TO_GLOBAL
139        sSystemGlobalKeys = new HashSet<String>();
140        Settings.System.getNonLegacyMovedKeys(sSystemGlobalKeys);
141
142        sRestrictedKeys = new HashMap<String, String>();
143        sRestrictedKeys.put(Settings.Secure.LOCATION_MODE, UserManager.DISALLOW_SHARE_LOCATION);
144        sRestrictedKeys.put(Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
145                UserManager.DISALLOW_SHARE_LOCATION);
146        sRestrictedKeys.put(Settings.Secure.INSTALL_NON_MARKET_APPS,
147                UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES);
148        sRestrictedKeys.put(Settings.Global.ADB_ENABLED, UserManager.DISALLOW_DEBUGGING_FEATURES);
149        sRestrictedKeys.put(Settings.Global.PACKAGE_VERIFIER_ENABLE,
150                UserManager.ENSURE_VERIFY_APPS);
151        sRestrictedKeys.put(Settings.Global.PREFERRED_NETWORK_MODE,
152                UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS);
153
154        sSecureCloneToManagedKeys = new HashSet<String>();
155        for (int i = 0; i < Settings.Secure.CLONE_TO_MANAGED_PROFILE.length; i++) {
156            sSecureCloneToManagedKeys.add(Settings.Secure.CLONE_TO_MANAGED_PROFILE[i]);
157        }
158        sSystemCloneToManagedKeys = new HashSet<String>();
159        for (int i = 0; i < Settings.System.CLONE_TO_MANAGED_PROFILE.length; i++) {
160            sSystemCloneToManagedKeys.add(Settings.System.CLONE_TO_MANAGED_PROFILE[i]);
161        }
162    }
163
164    private boolean settingMovedToGlobal(final String name) {
165        return sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name);
166    }
167
168    /**
169     * Decode a content URL into the table, projection, and arguments
170     * used to access the corresponding database rows.
171     */
172    private static class SqlArguments {
173        public String table;
174        public final String where;
175        public final String[] args;
176
177        /** Operate on existing rows. */
178        SqlArguments(Uri url, String where, String[] args) {
179            if (url.getPathSegments().size() == 1) {
180                // of the form content://settings/secure, arbitrary where clause
181                this.table = url.getPathSegments().get(0);
182                if (!DatabaseHelper.isValidTable(this.table)) {
183                    throw new IllegalArgumentException("Bad root path: " + this.table);
184                }
185                this.where = where;
186                this.args = args;
187            } else if (url.getPathSegments().size() != 2) {
188                throw new IllegalArgumentException("Invalid URI: " + url);
189            } else if (!TextUtils.isEmpty(where)) {
190                throw new UnsupportedOperationException("WHERE clause not supported: " + url);
191            } else {
192                // of the form content://settings/secure/element_name, no where clause
193                this.table = url.getPathSegments().get(0);
194                if (!DatabaseHelper.isValidTable(this.table)) {
195                    throw new IllegalArgumentException("Bad root path: " + this.table);
196                }
197                if (TABLE_SYSTEM.equals(this.table) || TABLE_SECURE.equals(this.table) ||
198                    TABLE_GLOBAL.equals(this.table)) {
199                    this.where = Settings.NameValueTable.NAME + "=?";
200                    final String name = url.getPathSegments().get(1);
201                    this.args = new String[] { name };
202                    // Rewrite the table for known-migrated names
203                    if (TABLE_SYSTEM.equals(this.table) || TABLE_SECURE.equals(this.table)) {
204                        if (sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name)) {
205                            this.table = TABLE_GLOBAL;
206                        }
207                    }
208                } else {
209                    // of the form content://bookmarks/19
210                    this.where = "_id=" + ContentUris.parseId(url);
211                    this.args = null;
212                }
213            }
214        }
215
216        /** Insert new rows (no where clause allowed). */
217        SqlArguments(Uri url) {
218            if (url.getPathSegments().size() == 1) {
219                this.table = url.getPathSegments().get(0);
220                if (!DatabaseHelper.isValidTable(this.table)) {
221                    throw new IllegalArgumentException("Bad root path: " + this.table);
222                }
223                this.where = null;
224                this.args = null;
225            } else {
226                throw new IllegalArgumentException("Invalid URI: " + url);
227            }
228        }
229    }
230
231    /**
232     * Get the content URI of a row added to a table.
233     * @param tableUri of the entire table
234     * @param values found in the row
235     * @param rowId of the row
236     * @return the content URI for this particular row
237     */
238    private Uri getUriFor(Uri tableUri, ContentValues values, long rowId) {
239        if (tableUri.getPathSegments().size() != 1) {
240            throw new IllegalArgumentException("Invalid URI: " + tableUri);
241        }
242        String table = tableUri.getPathSegments().get(0);
243        if (TABLE_SYSTEM.equals(table) ||
244                TABLE_SECURE.equals(table) ||
245                TABLE_GLOBAL.equals(table)) {
246            String name = values.getAsString(Settings.NameValueTable.NAME);
247            return Uri.withAppendedPath(tableUri, name);
248        } else {
249            return ContentUris.withAppendedId(tableUri, rowId);
250        }
251    }
252
253    /**
254     * Send a notification when a particular content URI changes.
255     * Modify the system property used to communicate the version of
256     * this table, for tables which have such a property.  (The Settings
257     * contract class uses these to provide client-side caches.)
258     * @param uri to send notifications for
259     */
260    private void sendNotify(Uri uri, int userHandle) {
261        // Update the system property *first*, so if someone is listening for
262        // a notification and then using the contract class to get their data,
263        // the system property will be updated and they'll get the new data.
264
265        boolean backedUpDataChanged = false;
266        String property = null, table = uri.getPathSegments().get(0);
267        final boolean isGlobal = table.equals(TABLE_GLOBAL);
268        if (table.equals(TABLE_SYSTEM)) {
269            property = Settings.System.SYS_PROP_SETTING_VERSION;
270            backedUpDataChanged = true;
271        } else if (table.equals(TABLE_SECURE)) {
272            property = Settings.Secure.SYS_PROP_SETTING_VERSION;
273            backedUpDataChanged = true;
274        } else if (isGlobal) {
275            property = Settings.Global.SYS_PROP_SETTING_VERSION;    // this one is global
276            backedUpDataChanged = true;
277        }
278
279        if (property != null) {
280            long version = SystemProperties.getLong(property, 0) + 1;
281            if (LOCAL_LOGV) Log.v(TAG, "property: " + property + "=" + version);
282            SystemProperties.set(property, Long.toString(version));
283        }
284
285        // Inform the backup manager about a data change
286        if (backedUpDataChanged) {
287            mBackupManager.dataChanged();
288        }
289        // Now send the notification through the content framework.
290
291        String notify = uri.getQueryParameter("notify");
292        if (notify == null || "true".equals(notify)) {
293            final int notifyTarget = isGlobal ? UserHandle.USER_ALL : userHandle;
294            final long oldId = Binder.clearCallingIdentity();
295            try {
296                getContext().getContentResolver().notifyChange(uri, null, true, notifyTarget);
297            } finally {
298                Binder.restoreCallingIdentity(oldId);
299            }
300            if (LOCAL_LOGV) Log.v(TAG, "notifying for " + notifyTarget + ": " + uri);
301        } else {
302            if (LOCAL_LOGV) Log.v(TAG, "notification suppressed: " + uri);
303        }
304    }
305
306    /**
307     * Make sure the caller has permission to write this data.
308     * @param args supplied by the caller
309     * @throws SecurityException if the caller is forbidden to write.
310     */
311    private void checkWritePermissions(SqlArguments args) {
312        if ((TABLE_SECURE.equals(args.table) || TABLE_GLOBAL.equals(args.table)) &&
313            getContext().checkCallingOrSelfPermission(
314                    android.Manifest.permission.WRITE_SECURE_SETTINGS) !=
315            PackageManager.PERMISSION_GRANTED) {
316            throw new SecurityException(
317                    String.format("Permission denial: writing to secure settings requires %1$s",
318                                  android.Manifest.permission.WRITE_SECURE_SETTINGS));
319        }
320    }
321
322    private void checkUserRestrictions(String setting, int userId) {
323        String userRestriction = sRestrictedKeys.get(setting);
324        if (!TextUtils.isEmpty(userRestriction)
325            && mUserManager.hasUserRestriction(userRestriction, new UserHandle(userId))) {
326            throw new SecurityException(
327                    "Permission denial: user is restricted from changing this setting.");
328        }
329    }
330
331    // FileObserver for external modifications to the database file.
332    // Note that this is for platform developers only with
333    // userdebug/eng builds who should be able to tinker with the
334    // sqlite database out from under the SettingsProvider, which is
335    // normally the exclusive owner of the database.  But we keep this
336    // enabled all the time to minimize development-vs-user
337    // differences in testing.
338    private static SparseArray<SettingsFileObserver> sObserverInstances
339            = new SparseArray<SettingsFileObserver>();
340    private class SettingsFileObserver extends FileObserver {
341        private final AtomicBoolean mIsDirty = new AtomicBoolean(false);
342        private final int mUserHandle;
343        private final String mPath;
344
345        public SettingsFileObserver(int userHandle, String path) {
346            super(path, FileObserver.CLOSE_WRITE |
347                  FileObserver.CREATE | FileObserver.DELETE |
348                  FileObserver.MOVED_TO | FileObserver.MODIFY);
349            mUserHandle = userHandle;
350            mPath = path;
351        }
352
353        public void onEvent(int event, String path) {
354            final AtomicInteger mutationCount;
355            synchronized (SettingsProvider.this) {
356                mutationCount = sKnownMutationsInFlight.get(mUserHandle);
357            }
358            if (mutationCount != null && mutationCount.get() > 0) {
359                // our own modification.
360                return;
361            }
362            Log.d(TAG, "User " + mUserHandle + " external modification to " + mPath
363                    + "; event=" + event);
364            if (!mIsDirty.compareAndSet(false, true)) {
365                // already handled. (we get a few update events
366                // during an sqlite write)
367                return;
368            }
369            Log.d(TAG, "User " + mUserHandle + " updating our caches for " + mPath);
370            fullyPopulateCaches(mUserHandle);
371            mIsDirty.set(false);
372        }
373    }
374
375    @Override
376    public boolean onCreate() {
377        mBackupManager = new BackupManager(getContext());
378        mUserManager = UserManager.get(getContext());
379
380        setAppOps(AppOpsManager.OP_NONE, AppOpsManager.OP_WRITE_SETTINGS);
381        establishDbTracking(UserHandle.USER_OWNER);
382
383        IntentFilter userFilter = new IntentFilter();
384        userFilter.addAction(Intent.ACTION_USER_REMOVED);
385        userFilter.addAction(Intent.ACTION_USER_ADDED);
386        getContext().registerReceiver(new BroadcastReceiver() {
387            @Override
388            public void onReceive(Context context, Intent intent) {
389                final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE,
390                        UserHandle.USER_OWNER);
391                if (intent.getAction().equals(Intent.ACTION_USER_REMOVED)) {
392                    onUserRemoved(userHandle);
393                } else if (intent.getAction().equals(Intent.ACTION_USER_ADDED)) {
394                    onProfilesChanged();
395                }
396            }
397        }, userFilter);
398
399        onProfilesChanged();
400
401        return true;
402    }
403
404    void onUserRemoved(int userHandle) {
405        synchronized (this) {
406            // the db file itself will be deleted automatically, but we need to tear down
407            // our caches and other internal bookkeeping.
408            FileObserver observer = sObserverInstances.get(userHandle);
409            if (observer != null) {
410                observer.stopWatching();
411                sObserverInstances.delete(userHandle);
412            }
413
414            mOpenHelpers.delete(userHandle);
415            sSystemCaches.delete(userHandle);
416            sSecureCaches.delete(userHandle);
417            sKnownMutationsInFlight.delete(userHandle);
418            onProfilesChanged();
419        }
420    }
421
422    /**
423     * Updates the list of managed profiles. It assumes that only the primary user
424     * can have managed profiles. Modify this code if that changes in the future.
425     */
426    void onProfilesChanged() {
427        synchronized (this) {
428            mManagedProfiles = mUserManager.getProfiles(UserHandle.USER_OWNER);
429            if (mManagedProfiles != null) {
430                // Remove the primary user from the list
431                for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
432                    if (mManagedProfiles.get(i).id == UserHandle.USER_OWNER) {
433                        mManagedProfiles.remove(i);
434                    }
435                }
436                // If there are no managed profiles, reset the variable
437                if (mManagedProfiles.size() == 0) {
438                    mManagedProfiles = null;
439                }
440            }
441            if (LOCAL_LOGV) {
442                Slog.d(TAG, "Managed Profiles = " + mManagedProfiles);
443            }
444        }
445    }
446
447    private void establishDbTracking(int userHandle) {
448        if (LOCAL_LOGV) {
449            Slog.i(TAG, "Installing settings db helper and caches for user " + userHandle);
450        }
451
452        DatabaseHelper dbhelper;
453
454        synchronized (this) {
455            dbhelper = mOpenHelpers.get(userHandle);
456            if (dbhelper == null) {
457                dbhelper = new DatabaseHelper(getContext(), userHandle);
458                mOpenHelpers.append(userHandle, dbhelper);
459
460                sSystemCaches.append(userHandle, new SettingsCache(TABLE_SYSTEM));
461                sSecureCaches.append(userHandle, new SettingsCache(TABLE_SECURE));
462                sKnownMutationsInFlight.append(userHandle, new AtomicInteger(0));
463            }
464        }
465
466        // Initialization of the db *outside* the locks.  It's possible that racing
467        // threads might wind up here, the second having read the cache entries
468        // written by the first, but that's benign: the SQLite helper implementation
469        // manages concurrency itself, and it's important that we not run the db
470        // initialization with any of our own locks held, so we're fine.
471        SQLiteDatabase db = dbhelper.getWritableDatabase();
472
473        // Watch for external modifications to the database files,
474        // keeping our caches in sync.  We synchronize the observer set
475        // separately, and of course it has to run after the db file
476        // itself was set up by the DatabaseHelper.
477        synchronized (sObserverInstances) {
478            if (sObserverInstances.get(userHandle) == null) {
479                SettingsFileObserver observer = new SettingsFileObserver(userHandle, db.getPath());
480                sObserverInstances.append(userHandle, observer);
481                observer.startWatching();
482            }
483        }
484
485        ensureAndroidIdIsSet(userHandle);
486
487        startAsyncCachePopulation(userHandle);
488    }
489
490    class CachePrefetchThread extends Thread {
491        private int mUserHandle;
492
493        CachePrefetchThread(int userHandle) {
494            super("populate-settings-caches");
495            mUserHandle = userHandle;
496        }
497
498        @Override
499        public void run() {
500            fullyPopulateCaches(mUserHandle);
501        }
502    }
503
504    private void startAsyncCachePopulation(int userHandle) {
505        new CachePrefetchThread(userHandle).start();
506    }
507
508    private void fullyPopulateCaches(final int userHandle) {
509        DatabaseHelper dbHelper;
510        synchronized (this) {
511            dbHelper = mOpenHelpers.get(userHandle);
512        }
513        if (dbHelper == null) {
514            // User is gone.
515            return;
516        }
517        // Only populate the globals cache once, for the owning user
518        if (userHandle == UserHandle.USER_OWNER) {
519            fullyPopulateCache(dbHelper, TABLE_GLOBAL, sGlobalCache);
520        }
521        fullyPopulateCache(dbHelper, TABLE_SECURE, sSecureCaches.get(userHandle));
522        fullyPopulateCache(dbHelper, TABLE_SYSTEM, sSystemCaches.get(userHandle));
523    }
524
525    // Slurp all values (if sane in number & size) into cache.
526    private void fullyPopulateCache(DatabaseHelper dbHelper, String table, SettingsCache cache) {
527        SQLiteDatabase db = dbHelper.getReadableDatabase();
528        Cursor c = db.query(
529            table,
530            new String[] { Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE },
531            null, null, null, null, null,
532            "" + (MAX_CACHE_ENTRIES + 1) /* limit */);
533        try {
534            synchronized (cache) {
535                cache.evictAll();
536                cache.setFullyMatchesDisk(true);  // optimistic
537                int rows = 0;
538                while (c.moveToNext()) {
539                    rows++;
540                    String name = c.getString(0);
541                    String value = c.getString(1);
542                    cache.populate(name, value);
543                }
544                if (rows > MAX_CACHE_ENTRIES) {
545                    // Somewhat redundant, as removeEldestEntry() will
546                    // have already done this, but to be explicit:
547                    cache.setFullyMatchesDisk(false);
548                    Log.d(TAG, "row count exceeds max cache entries for table " + table);
549                }
550                if (LOCAL_LOGV) Log.d(TAG, "cache for settings table '" + table
551                        + "' rows=" + rows + "; fullycached=" + cache.fullyMatchesDisk());
552            }
553        } finally {
554            c.close();
555        }
556    }
557
558    private boolean ensureAndroidIdIsSet(int userHandle) {
559        final Cursor c = queryForUser(Settings.Secure.CONTENT_URI,
560                new String[] { Settings.NameValueTable.VALUE },
561                Settings.NameValueTable.NAME + "=?",
562                new String[] { Settings.Secure.ANDROID_ID }, null,
563                userHandle);
564        try {
565            final String value = c.moveToNext() ? c.getString(0) : null;
566            if (value == null) {
567                // sanity-check the user before touching the db
568                final UserInfo user = mUserManager.getUserInfo(userHandle);
569                if (user == null) {
570                    // can happen due to races when deleting users; treat as benign
571                    return false;
572                }
573
574                final SecureRandom random = new SecureRandom();
575                final String newAndroidIdValue = Long.toHexString(random.nextLong());
576                final ContentValues values = new ContentValues();
577                values.put(Settings.NameValueTable.NAME, Settings.Secure.ANDROID_ID);
578                values.put(Settings.NameValueTable.VALUE, newAndroidIdValue);
579                final Uri uri = insertForUser(Settings.Secure.CONTENT_URI, values, userHandle);
580                if (uri == null) {
581                    Slog.e(TAG, "Unable to generate new ANDROID_ID for user " + userHandle);
582                    return false;
583                }
584                Slog.d(TAG, "Generated and saved new ANDROID_ID [" + newAndroidIdValue
585                        + "] for user " + userHandle);
586                // Write a dropbox entry if it's a restricted profile
587                if (user.isRestricted()) {
588                    DropBoxManager dbm = (DropBoxManager)
589                            getContext().getSystemService(Context.DROPBOX_SERVICE);
590                    if (dbm != null && dbm.isTagEnabled(DROPBOX_TAG_USERLOG)) {
591                        dbm.addText(DROPBOX_TAG_USERLOG, System.currentTimeMillis()
592                                + ",restricted_profile_ssaid,"
593                                + newAndroidIdValue + "\n");
594                    }
595                }
596            }
597            return true;
598        } finally {
599            c.close();
600        }
601    }
602
603    // Lazy-initialize the settings caches for non-primary users
604    private SettingsCache getOrConstructCache(int callingUser, SparseArray<SettingsCache> which) {
605        getOrEstablishDatabase(callingUser); // ignore return value; we don't need it
606        return which.get(callingUser);
607    }
608
609    // Lazy initialize the database helper and caches for this user, if necessary
610    private DatabaseHelper getOrEstablishDatabase(int callingUser) {
611        if (callingUser >= Process.SYSTEM_UID) {
612            if (USER_CHECK_THROWS) {
613                throw new IllegalArgumentException("Uid rather than user handle: " + callingUser);
614            } else {
615                Slog.wtf(TAG, "establish db for uid rather than user: " + callingUser);
616            }
617        }
618
619        long oldId = Binder.clearCallingIdentity();
620        try {
621            DatabaseHelper dbHelper;
622            synchronized (this) {
623                dbHelper = mOpenHelpers.get(callingUser);
624            }
625            if (null == dbHelper) {
626                establishDbTracking(callingUser);
627                synchronized (this) {
628                    dbHelper = mOpenHelpers.get(callingUser);
629                }
630            }
631            return dbHelper;
632        } finally {
633            Binder.restoreCallingIdentity(oldId);
634        }
635    }
636
637    public SettingsCache cacheForTable(final int callingUser, String tableName) {
638        if (TABLE_SYSTEM.equals(tableName)) {
639            return getOrConstructCache(callingUser, sSystemCaches);
640        }
641        if (TABLE_SECURE.equals(tableName)) {
642            return getOrConstructCache(callingUser, sSecureCaches);
643        }
644        if (TABLE_GLOBAL.equals(tableName)) {
645            return sGlobalCache;
646        }
647        return null;
648    }
649
650    /**
651     * Used for wiping a whole cache on deletes when we're not
652     * sure what exactly was deleted or changed.
653     */
654    public void invalidateCache(final int callingUser, String tableName) {
655        SettingsCache cache = cacheForTable(callingUser, tableName);
656        if (cache == null) {
657            return;
658        }
659        synchronized (cache) {
660            cache.evictAll();
661            cache.mCacheFullyMatchesDisk = false;
662        }
663    }
664
665    /**
666     * Checks if the calling user is a managed profile of the primary user.
667     * Currently only the primary user (USER_OWNER) can have managed profiles.
668     * @param callingUser the user trying to read/write settings
669     * @return true if it is a managed profile of the primary user
670     */
671    private boolean isManagedProfile(int callingUser) {
672        synchronized (this) {
673            if (mManagedProfiles == null) return false;
674            for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
675                if (mManagedProfiles.get(i).id == callingUser) {
676                    return true;
677                }
678            }
679            return false;
680        }
681    }
682
683    /**
684     * Fast path that avoids the use of chatty remoted Cursors.
685     */
686    @Override
687    public Bundle call(String method, String request, Bundle args) {
688        int callingUser = UserHandle.getCallingUserId();
689        if (args != null) {
690            int reqUser = args.getInt(Settings.CALL_METHOD_USER_KEY, callingUser);
691            if (reqUser != callingUser) {
692                callingUser = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
693                        Binder.getCallingUid(), reqUser, false, true,
694                        "get/set setting for user", null);
695                if (LOCAL_LOGV) Slog.v(TAG, "   access setting for user " + callingUser);
696            }
697        }
698
699        // Note: we assume that get/put operations for moved-to-global names have already
700        // been directed to the new location on the caller side (otherwise we'd fix them
701        // up here).
702        DatabaseHelper dbHelper;
703        SettingsCache cache;
704
705        // Get methods
706        if (Settings.CALL_METHOD_GET_SYSTEM.equals(method)) {
707            if (LOCAL_LOGV) Slog.v(TAG, "call(system:" + request + ") for " + callingUser);
708            // Check if this request should be (re)directed to the primary user's db
709            if (callingUser != UserHandle.USER_OWNER
710                    && shouldShadowParentProfile(callingUser, sSystemCloneToManagedKeys, request)) {
711                callingUser = UserHandle.USER_OWNER;
712            }
713            dbHelper = getOrEstablishDatabase(callingUser);
714            cache = sSystemCaches.get(callingUser);
715            return lookupValue(dbHelper, TABLE_SYSTEM, cache, request);
716        }
717        if (Settings.CALL_METHOD_GET_SECURE.equals(method)) {
718            if (LOCAL_LOGV) Slog.v(TAG, "call(secure:" + request + ") for " + callingUser);
719            // Check if this is a setting to be copied from the primary user
720            if (shouldShadowParentProfile(callingUser, sSecureCloneToManagedKeys, request)) {
721                // If the request if for location providers and there's a restriction, return none
722                if (Secure.LOCATION_PROVIDERS_ALLOWED.equals(request)
723                        && mUserManager.hasUserRestriction(
724                                UserManager.DISALLOW_SHARE_LOCATION, new UserHandle(callingUser))) {
725                    return sSecureCaches.get(callingUser).putIfAbsent(request, "");
726                }
727                callingUser = UserHandle.USER_OWNER;
728            }
729            dbHelper = getOrEstablishDatabase(callingUser);
730            cache = sSecureCaches.get(callingUser);
731            return lookupValue(dbHelper, TABLE_SECURE, cache, request);
732        }
733        if (Settings.CALL_METHOD_GET_GLOBAL.equals(method)) {
734            if (LOCAL_LOGV) Slog.v(TAG, "call(global:" + request + ") for " + callingUser);
735            // fast path: owner db & cache are immutable after onCreate() so we need not
736            // guard on the attempt to look them up
737            return lookupValue(getOrEstablishDatabase(UserHandle.USER_OWNER), TABLE_GLOBAL,
738                    sGlobalCache, request);
739        }
740
741        // Put methods - new value is in the args bundle under the key named by
742        // the Settings.NameValueTable.VALUE static.
743        final String newValue = (args == null)
744                ? null : args.getString(Settings.NameValueTable.VALUE);
745
746        // Framework can't do automatic permission checking for calls, so we need
747        // to do it here.
748        if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.WRITE_SETTINGS)
749                != PackageManager.PERMISSION_GRANTED) {
750            throw new SecurityException(
751                    String.format("Permission denial: writing to settings requires %1$s",
752                                  android.Manifest.permission.WRITE_SETTINGS));
753        }
754
755        // Also need to take care of app op.
756        if (getAppOpsManager().noteOp(AppOpsManager.OP_WRITE_SETTINGS, Binder.getCallingUid(),
757                getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
758            return null;
759        }
760
761        final ContentValues values = new ContentValues();
762        values.put(Settings.NameValueTable.NAME, request);
763        values.put(Settings.NameValueTable.VALUE, newValue);
764        if (Settings.CALL_METHOD_PUT_SYSTEM.equals(method)) {
765            if (LOCAL_LOGV) {
766                Slog.v(TAG, "call_put(system:" + request + "=" + newValue + ") for "
767                        + callingUser);
768            }
769            // Extra check for USER_OWNER to optimize for the 99%
770            if (callingUser != UserHandle.USER_OWNER && shouldShadowParentProfile(callingUser,
771                    sSystemCloneToManagedKeys, request)) {
772                // Don't write these settings, as they are cloned from the parent profile
773                return null;
774            }
775            insertForUser(Settings.System.CONTENT_URI, values, callingUser);
776            // Clone the settings to the managed profiles so that notifications can be sent out
777            if (callingUser == UserHandle.USER_OWNER && mManagedProfiles != null
778                    && sSystemCloneToManagedKeys.contains(request)) {
779                final long token = Binder.clearCallingIdentity();
780                try {
781                    for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
782                        if (LOCAL_LOGV) {
783                            Slog.v(TAG, "putting to additional user "
784                                    + mManagedProfiles.get(i).id);
785                        }
786                        insertForUser(Settings.System.CONTENT_URI, values,
787                                mManagedProfiles.get(i).id);
788                    }
789                } finally {
790                    Binder.restoreCallingIdentity(token);
791                }
792            }
793        } else if (Settings.CALL_METHOD_PUT_SECURE.equals(method)) {
794            if (LOCAL_LOGV) {
795                Slog.v(TAG, "call_put(secure:" + request + "=" + newValue + ") for "
796                        + callingUser);
797            }
798            // Extra check for USER_OWNER to optimize for the 99%
799            if (callingUser != UserHandle.USER_OWNER && shouldShadowParentProfile(callingUser,
800                    sSecureCloneToManagedKeys, request)) {
801                // Don't write these settings, as they are cloned from the parent profile
802                return null;
803            }
804            insertForUser(Settings.Secure.CONTENT_URI, values, callingUser);
805            // Clone the settings to the managed profiles so that notifications can be sent out
806            if (callingUser == UserHandle.USER_OWNER && mManagedProfiles != null
807                    && sSecureCloneToManagedKeys.contains(request)) {
808                final long token = Binder.clearCallingIdentity();
809                try {
810                    for (int i = mManagedProfiles.size() - 1; i >= 0; i--) {
811                        if (LOCAL_LOGV) {
812                            Slog.v(TAG, "putting to additional user "
813                                    + mManagedProfiles.get(i).id);
814                        }
815                        try {
816                            insertForUser(Settings.Secure.CONTENT_URI, values,
817                                    mManagedProfiles.get(i).id);
818                        } catch (SecurityException e) {
819                            // Temporary fix, see b/17450158
820                            Slog.w(TAG, "Cannot clone request '" + request + "' with value '"
821                                    + newValue + "' to managed profile (id "
822                                    + mManagedProfiles.get(i).id + ")", e);
823                        }
824                    }
825                } finally {
826                    Binder.restoreCallingIdentity(token);
827                }
828            }
829        } else if (Settings.CALL_METHOD_PUT_GLOBAL.equals(method)) {
830            if (LOCAL_LOGV) {
831                Slog.v(TAG, "call_put(global:" + request + "=" + newValue + ") for "
832                        + callingUser);
833            }
834            insertForUser(Settings.Global.CONTENT_URI, values, callingUser);
835        } else {
836            Slog.w(TAG, "call() with invalid method: " + method);
837        }
838
839        return null;
840    }
841
842    /**
843     * Check if the user is a managed profile and name is one of the settings to be cloned
844     * from the parent profile.
845     */
846    private boolean shouldShadowParentProfile(int userId, HashSet<String> keys, String name) {
847        return isManagedProfile(userId) && keys.contains(name);
848    }
849
850    // Looks up value 'key' in 'table' and returns either a single-pair Bundle,
851    // possibly with a null value, or null on failure.
852    private Bundle lookupValue(DatabaseHelper dbHelper, String table,
853            final SettingsCache cache, String key) {
854        if (cache == null) {
855           Slog.e(TAG, "cache is null for user " + UserHandle.getCallingUserId() + " : key=" + key);
856           return null;
857        }
858        synchronized (cache) {
859            Bundle value = cache.get(key);
860            if (value != null) {
861                if (value != TOO_LARGE_TO_CACHE_MARKER) {
862                    return value;
863                }
864                // else we fall through and read the value from disk
865            } else if (cache.fullyMatchesDisk()) {
866                // Fast path (very common).  Don't even try touch disk
867                // if we know we've slurped it all in.  Trying to
868                // touch the disk would mean waiting for yaffs2 to
869                // give us access, which could takes hundreds of
870                // milliseconds.  And we're very likely being called
871                // from somebody's UI thread...
872                return NULL_SETTING;
873            }
874        }
875
876        SQLiteDatabase db = dbHelper.getReadableDatabase();
877        Cursor cursor = null;
878        try {
879            cursor = db.query(table, COLUMN_VALUE, "name=?", new String[]{key},
880                              null, null, null, null);
881            if (cursor != null && cursor.getCount() == 1) {
882                cursor.moveToFirst();
883                return cache.putIfAbsent(key, cursor.getString(0));
884            }
885        } catch (SQLiteException e) {
886            Log.w(TAG, "settings lookup error", e);
887            return null;
888        } finally {
889            if (cursor != null) cursor.close();
890        }
891        cache.putIfAbsent(key, null);
892        return NULL_SETTING;
893    }
894
895    @Override
896    public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) {
897        return queryForUser(url, select, where, whereArgs, sort, UserHandle.getCallingUserId());
898    }
899
900    private Cursor queryForUser(Uri url, String[] select, String where, String[] whereArgs,
901            String sort, int forUser) {
902        if (LOCAL_LOGV) Slog.v(TAG, "query(" + url + ") for user " + forUser);
903        SqlArguments args = new SqlArguments(url, where, whereArgs);
904        DatabaseHelper dbH;
905        dbH = getOrEstablishDatabase(
906                TABLE_GLOBAL.equals(args.table) ? UserHandle.USER_OWNER : forUser);
907        SQLiteDatabase db = dbH.getReadableDatabase();
908
909        // The favorites table was moved from this provider to a provider inside Home
910        // Home still need to query this table to upgrade from pre-cupcake builds
911        // However, a cupcake+ build with no data does not contain this table which will
912        // cause an exception in the SQL stack. The following line is a special case to
913        // let the caller of the query have a chance to recover and avoid the exception
914        if (TABLE_FAVORITES.equals(args.table)) {
915            return null;
916        } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
917            args.table = TABLE_FAVORITES;
918            Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null);
919            if (cursor != null) {
920                boolean exists = cursor.getCount() > 0;
921                cursor.close();
922                if (!exists) return null;
923            } else {
924                return null;
925            }
926        }
927
928        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
929        qb.setTables(args.table);
930
931        Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort);
932        // the default Cursor interface does not support per-user observation
933        try {
934            AbstractCursor c = (AbstractCursor) ret;
935            c.setNotificationUri(getContext().getContentResolver(), url, forUser);
936        } catch (ClassCastException e) {
937            // details of the concrete Cursor implementation have changed and this code has
938            // not been updated to match -- complain and fail hard.
939            Log.wtf(TAG, "Incompatible cursor derivation!");
940            throw e;
941        }
942        return ret;
943    }
944
945    @Override
946    public String getType(Uri url) {
947        // If SqlArguments supplies a where clause, then it must be an item
948        // (because we aren't supplying our own where clause).
949        SqlArguments args = new SqlArguments(url, null, null);
950        if (TextUtils.isEmpty(args.where)) {
951            return "vnd.android.cursor.dir/" + args.table;
952        } else {
953            return "vnd.android.cursor.item/" + args.table;
954        }
955    }
956
957    @Override
958    public int bulkInsert(Uri uri, ContentValues[] values) {
959        final int callingUser = UserHandle.getCallingUserId();
960        if (LOCAL_LOGV) Slog.v(TAG, "bulkInsert() for user " + callingUser);
961        SqlArguments args = new SqlArguments(uri);
962        if (TABLE_FAVORITES.equals(args.table)) {
963            return 0;
964        }
965        checkWritePermissions(args);
966        SettingsCache cache = cacheForTable(callingUser, args.table);
967
968        final AtomicInteger mutationCount;
969        synchronized (this) {
970            mutationCount = sKnownMutationsInFlight.get(callingUser);
971        }
972        if (mutationCount != null) {
973            mutationCount.incrementAndGet();
974        }
975        DatabaseHelper dbH = getOrEstablishDatabase(
976                TABLE_GLOBAL.equals(args.table) ? UserHandle.USER_OWNER : callingUser);
977        SQLiteDatabase db = dbH.getWritableDatabase();
978        db.beginTransaction();
979        try {
980            int numValues = values.length;
981            for (int i = 0; i < numValues; i++) {
982                checkUserRestrictions(values[i].getAsString(Settings.Secure.NAME), callingUser);
983                if (db.insert(args.table, null, values[i]) < 0) return 0;
984                SettingsCache.populate(cache, values[i]);
985                if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]);
986            }
987            db.setTransactionSuccessful();
988        } finally {
989            db.endTransaction();
990            if (mutationCount != null) {
991                mutationCount.decrementAndGet();
992            }
993        }
994
995        sendNotify(uri, callingUser);
996        return values.length;
997    }
998
999    /*
1000     * Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED.
1001     * This setting contains a list of the currently enabled location providers.
1002     * But helper functions in android.providers.Settings can enable or disable
1003     * a single provider by using a "+" or "-" prefix before the provider name.
1004     *
1005     * @returns whether the database needs to be updated or not, also modifying
1006     *     'initialValues' if needed.
1007     */
1008    private boolean parseProviderList(Uri url, ContentValues initialValues, int desiredUser) {
1009        String value = initialValues.getAsString(Settings.Secure.VALUE);
1010        String newProviders = null;
1011        if (value != null && value.length() > 1) {
1012            char prefix = value.charAt(0);
1013            if (prefix == '+' || prefix == '-') {
1014                // skip prefix
1015                value = value.substring(1);
1016
1017                // read list of enabled providers into "providers"
1018                String providers = "";
1019                String[] columns = {Settings.Secure.VALUE};
1020                String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'";
1021                Cursor cursor = queryForUser(url, columns, where, null, null, desiredUser);
1022                if (cursor != null && cursor.getCount() == 1) {
1023                    try {
1024                        cursor.moveToFirst();
1025                        providers = cursor.getString(0);
1026                    } finally {
1027                        cursor.close();
1028                    }
1029                }
1030
1031                int index = providers.indexOf(value);
1032                int end = index + value.length();
1033                // check for commas to avoid matching on partial string
1034                if (index > 0 && providers.charAt(index - 1) != ',') index = -1;
1035                if (end < providers.length() && providers.charAt(end) != ',') index = -1;
1036
1037                if (prefix == '+' && index < 0) {
1038                    // append the provider to the list if not present
1039                    if (providers.length() == 0) {
1040                        newProviders = value;
1041                    } else {
1042                        newProviders = providers + ',' + value;
1043                    }
1044                } else if (prefix == '-' && index >= 0) {
1045                    // remove the provider from the list if present
1046                    // remove leading or trailing comma
1047                    if (index > 0) {
1048                        index--;
1049                    } else if (end < providers.length()) {
1050                        end++;
1051                    }
1052
1053                    newProviders = providers.substring(0, index);
1054                    if (end < providers.length()) {
1055                        newProviders += providers.substring(end);
1056                    }
1057                } else {
1058                    // nothing changed, so no need to update the database
1059                    return false;
1060                }
1061
1062                if (newProviders != null) {
1063                    initialValues.put(Settings.Secure.VALUE, newProviders);
1064                }
1065            }
1066        }
1067
1068        return true;
1069    }
1070
1071    @Override
1072    public Uri insert(Uri url, ContentValues initialValues) {
1073        return insertForUser(url, initialValues, UserHandle.getCallingUserId());
1074    }
1075
1076    // Settings.put*ForUser() always winds up here, so this is where we apply
1077    // policy around permission to write settings for other users.
1078    private Uri insertForUser(Uri url, ContentValues initialValues, int desiredUserHandle) {
1079        final int callingUser = UserHandle.getCallingUserId();
1080        if (callingUser != desiredUserHandle) {
1081            getContext().enforceCallingOrSelfPermission(
1082                    android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
1083                    "Not permitted to access settings for other users");
1084        }
1085
1086        if (LOCAL_LOGV) Slog.v(TAG, "insert(" + url + ") for user " + desiredUserHandle
1087                + " by " + callingUser);
1088
1089        SqlArguments args = new SqlArguments(url);
1090        if (TABLE_FAVORITES.equals(args.table)) {
1091            return null;
1092        }
1093
1094        // Special case LOCATION_PROVIDERS_ALLOWED.
1095        // Support enabling/disabling a single provider (using "+" or "-" prefix)
1096        String name = initialValues.getAsString(Settings.Secure.NAME);
1097        if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
1098            if (!parseProviderList(url, initialValues, desiredUserHandle)) return null;
1099        }
1100
1101        // If this is an insert() of a key that has been migrated to the global store,
1102        // redirect the operation to that store
1103        if (name != null) {
1104            if (sSecureGlobalKeys.contains(name) || sSystemGlobalKeys.contains(name)) {
1105                if (!TABLE_GLOBAL.equals(args.table)) {
1106                    if (LOCAL_LOGV) Slog.i(TAG, "Rewrite of insert() of now-global key " + name);
1107                }
1108                args.table = TABLE_GLOBAL;  // next condition will rewrite the user handle
1109            }
1110        }
1111
1112        // Check write permissions only after determining which table the insert will touch
1113        checkWritePermissions(args);
1114
1115        checkUserRestrictions(name, desiredUserHandle);
1116
1117        // The global table is stored under the owner, always
1118        if (TABLE_GLOBAL.equals(args.table)) {
1119            desiredUserHandle = UserHandle.USER_OWNER;
1120        }
1121
1122        SettingsCache cache = cacheForTable(desiredUserHandle, args.table);
1123        String value = initialValues.getAsString(Settings.NameValueTable.VALUE);
1124        if (SettingsCache.isRedundantSetValue(cache, name, value)) {
1125            return Uri.withAppendedPath(url, name);
1126        }
1127
1128        final AtomicInteger mutationCount;
1129        synchronized (this) {
1130            mutationCount = sKnownMutationsInFlight.get(callingUser);
1131        }
1132        if (mutationCount != null) {
1133            mutationCount.incrementAndGet();
1134        }
1135        DatabaseHelper dbH = getOrEstablishDatabase(desiredUserHandle);
1136        SQLiteDatabase db = dbH.getWritableDatabase();
1137        final long rowId = db.insert(args.table, null, initialValues);
1138        if (mutationCount != null) {
1139            mutationCount.decrementAndGet();
1140        }
1141        if (rowId <= 0) return null;
1142
1143        SettingsCache.populate(cache, initialValues);  // before we notify
1144
1145        if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues
1146                + " for user " + desiredUserHandle);
1147        // Note that we use the original url here, not the potentially-rewritten table name
1148        url = getUriFor(url, initialValues, rowId);
1149        sendNotify(url, desiredUserHandle);
1150        return url;
1151    }
1152
1153    @Override
1154    public int delete(Uri url, String where, String[] whereArgs) {
1155        int callingUser = UserHandle.getCallingUserId();
1156        if (LOCAL_LOGV) Slog.v(TAG, "delete() for user " + callingUser);
1157        SqlArguments args = new SqlArguments(url, where, whereArgs);
1158        if (TABLE_FAVORITES.equals(args.table)) {
1159            return 0;
1160        } else if (TABLE_OLD_FAVORITES.equals(args.table)) {
1161            args.table = TABLE_FAVORITES;
1162        } else if (TABLE_GLOBAL.equals(args.table)) {
1163            callingUser = UserHandle.USER_OWNER;
1164        }
1165        checkWritePermissions(args);
1166
1167        final AtomicInteger mutationCount;
1168        synchronized (this) {
1169            mutationCount = sKnownMutationsInFlight.get(callingUser);
1170        }
1171        if (mutationCount != null) {
1172            mutationCount.incrementAndGet();
1173        }
1174        DatabaseHelper dbH = getOrEstablishDatabase(callingUser);
1175        SQLiteDatabase db = dbH.getWritableDatabase();
1176        int count = db.delete(args.table, args.where, args.args);
1177        if (mutationCount != null) {
1178            mutationCount.decrementAndGet();
1179        }
1180        if (count > 0) {
1181            invalidateCache(callingUser, args.table);  // before we notify
1182            sendNotify(url, callingUser);
1183        }
1184        startAsyncCachePopulation(callingUser);
1185        if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted");
1186        return count;
1187    }
1188
1189    @Override
1190    public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) {
1191        // NOTE: update() is never called by the front-end Settings API, and updates that
1192        // wind up affecting rows in Secure that are globally shared will not have the
1193        // intended effect (the update will be invisible to the rest of the system).
1194        // This should have no practical effect, since writes to the Secure db can only
1195        // be done by system code, and that code should be using the correct API up front.
1196        int callingUser = UserHandle.getCallingUserId();
1197        if (LOCAL_LOGV) Slog.v(TAG, "update() for user " + callingUser);
1198        SqlArguments args = new SqlArguments(url, where, whereArgs);
1199        if (TABLE_FAVORITES.equals(args.table)) {
1200            return 0;
1201        } else if (TABLE_GLOBAL.equals(args.table)) {
1202            callingUser = UserHandle.USER_OWNER;
1203        }
1204        checkWritePermissions(args);
1205        checkUserRestrictions(initialValues.getAsString(Settings.Secure.NAME), callingUser);
1206
1207        final AtomicInteger mutationCount;
1208        synchronized (this) {
1209            mutationCount = sKnownMutationsInFlight.get(callingUser);
1210        }
1211        if (mutationCount != null) {
1212            mutationCount.incrementAndGet();
1213        }
1214        DatabaseHelper dbH = getOrEstablishDatabase(callingUser);
1215        SQLiteDatabase db = dbH.getWritableDatabase();
1216        int count = db.update(args.table, initialValues, args.where, args.args);
1217        if (mutationCount != null) {
1218            mutationCount.decrementAndGet();
1219        }
1220        if (count > 0) {
1221            invalidateCache(callingUser, args.table);  // before we notify
1222            sendNotify(url, callingUser);
1223        }
1224        startAsyncCachePopulation(callingUser);
1225        if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues);
1226        return count;
1227    }
1228
1229    @Override
1230    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
1231
1232        /*
1233         * When a client attempts to openFile the default ringtone or
1234         * notification setting Uri, we will proxy the call to the current
1235         * default ringtone's Uri (if it is in the media provider).
1236         */
1237        int ringtoneType = RingtoneManager.getDefaultType(uri);
1238        // Above call returns -1 if the Uri doesn't match a default type
1239        if (ringtoneType != -1) {
1240            Context context = getContext();
1241
1242            // Get the current value for the default sound
1243            Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
1244
1245            if (soundUri != null) {
1246                // Proxy the openFile call to media provider
1247                String authority = soundUri.getAuthority();
1248                if (authority.equals(MediaStore.AUTHORITY)) {
1249                    return context.getContentResolver().openFileDescriptor(soundUri, mode);
1250                }
1251            }
1252        }
1253
1254        return super.openFile(uri, mode);
1255    }
1256
1257    @Override
1258    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
1259
1260        /*
1261         * When a client attempts to openFile the default ringtone or
1262         * notification setting Uri, we will proxy the call to the current
1263         * default ringtone's Uri (if it is in the media provider).
1264         */
1265        int ringtoneType = RingtoneManager.getDefaultType(uri);
1266        // Above call returns -1 if the Uri doesn't match a default type
1267        if (ringtoneType != -1) {
1268            Context context = getContext();
1269
1270            // Get the current value for the default sound
1271            Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
1272
1273            if (soundUri != null) {
1274                // Proxy the openFile call to media provider
1275                String authority = soundUri.getAuthority();
1276                if (authority.equals(MediaStore.AUTHORITY)) {
1277                    ParcelFileDescriptor pfd = null;
1278                    try {
1279                        pfd = context.getContentResolver().openFileDescriptor(soundUri, mode);
1280                        return new AssetFileDescriptor(pfd, 0, -1);
1281                    } catch (FileNotFoundException ex) {
1282                        // fall through and open the fallback ringtone below
1283                    }
1284                }
1285
1286                try {
1287                    return super.openAssetFile(soundUri, mode);
1288                } catch (FileNotFoundException ex) {
1289                    // Since a non-null Uri was specified, but couldn't be opened,
1290                    // fall back to the built-in ringtone.
1291                    return context.getResources().openRawResourceFd(
1292                            com.android.internal.R.raw.fallbackring);
1293                }
1294            }
1295            // no need to fall through and have openFile() try again, since we
1296            // already know that will fail.
1297            throw new FileNotFoundException(); // or return null ?
1298        }
1299
1300        // Note that this will end up calling openFile() above.
1301        return super.openAssetFile(uri, mode);
1302    }
1303
1304    /**
1305     * In-memory LRU Cache of system and secure settings, along with
1306     * associated helper functions to keep cache coherent with the
1307     * database.
1308     */
1309    private static final class SettingsCache extends LruCache<String, Bundle> {
1310
1311        private final String mCacheName;
1312        private boolean mCacheFullyMatchesDisk = false;  // has the whole database slurped.
1313
1314        public SettingsCache(String name) {
1315            super(MAX_CACHE_ENTRIES);
1316            mCacheName = name;
1317        }
1318
1319        /**
1320         * Is the whole database table slurped into this cache?
1321         */
1322        public boolean fullyMatchesDisk() {
1323            synchronized (this) {
1324                return mCacheFullyMatchesDisk;
1325            }
1326        }
1327
1328        public void setFullyMatchesDisk(boolean value) {
1329            synchronized (this) {
1330                mCacheFullyMatchesDisk = value;
1331            }
1332        }
1333
1334        @Override
1335        protected void entryRemoved(boolean evicted, String key, Bundle oldValue, Bundle newValue) {
1336            if (evicted) {
1337                mCacheFullyMatchesDisk = false;
1338            }
1339        }
1340
1341        /**
1342         * Atomic cache population, conditional on size of value and if
1343         * we lost a race.
1344         *
1345         * @returns a Bundle to send back to the client from call(), even
1346         *     if we lost the race.
1347         */
1348        public Bundle putIfAbsent(String key, String value) {
1349            Bundle bundle = (value == null) ? NULL_SETTING : Bundle.forPair("value", value);
1350            if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
1351                synchronized (this) {
1352                    if (get(key) == null) {
1353                        put(key, bundle);
1354                    }
1355                }
1356            }
1357            return bundle;
1358        }
1359
1360        /**
1361         * Populates a key in a given (possibly-null) cache.
1362         */
1363        public static void populate(SettingsCache cache, ContentValues contentValues) {
1364            if (cache == null) {
1365                return;
1366            }
1367            String name = contentValues.getAsString(Settings.NameValueTable.NAME);
1368            if (name == null) {
1369                Log.w(TAG, "null name populating settings cache.");
1370                return;
1371            }
1372            String value = contentValues.getAsString(Settings.NameValueTable.VALUE);
1373            cache.populate(name, value);
1374        }
1375
1376        public void populate(String name, String value) {
1377            synchronized (this) {
1378                if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
1379                    put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value));
1380                } else {
1381                    put(name, TOO_LARGE_TO_CACHE_MARKER);
1382                }
1383            }
1384        }
1385
1386        /**
1387         * For suppressing duplicate/redundant settings inserts early,
1388         * checking our cache first (but without faulting it in),
1389         * before going to sqlite with the mutation.
1390         */
1391        public static boolean isRedundantSetValue(SettingsCache cache, String name, String value) {
1392            if (cache == null) return false;
1393            synchronized (cache) {
1394                Bundle bundle = cache.get(name);
1395                if (bundle == null) return false;
1396                String oldValue = bundle.getPairValue();
1397                if (oldValue == null && value == null) return true;
1398                if ((oldValue == null) != (value == null)) return false;
1399                return oldValue.equals(value);
1400            }
1401        }
1402    }
1403}
1404