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