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