Merge "Negatively cache settings and proactively slurp settings into cache." into gingerbread
This commit is contained in:
committed by
Android (Google) Code Review
commit
bb0eb5ca84
@@ -22,6 +22,8 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import android.app.backup.BackupManager;
|
import android.app.backup.BackupManager;
|
||||||
import android.content.ContentProvider;
|
import android.content.ContentProvider;
|
||||||
@@ -37,6 +39,7 @@ import android.database.sqlite.SQLiteQueryBuilder;
|
|||||||
import android.media.RingtoneManager;
|
import android.media.RingtoneManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.FileObserver;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.os.SystemProperties;
|
import android.os.SystemProperties;
|
||||||
import android.provider.DrmStore;
|
import android.provider.DrmStore;
|
||||||
@@ -56,9 +59,15 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
|
|
||||||
// Cache for settings, access-ordered for acting as LRU.
|
// Cache for settings, access-ordered for acting as LRU.
|
||||||
// Guarded by themselves.
|
// Guarded by themselves.
|
||||||
private static final int MAX_CACHE_ENTRIES = 50;
|
private static final int MAX_CACHE_ENTRIES = 200;
|
||||||
private static final SettingsCache sSystemCache = new SettingsCache();
|
private static final SettingsCache sSystemCache = new SettingsCache("system");
|
||||||
private static final SettingsCache sSecureCache = new SettingsCache();
|
private static final SettingsCache sSecureCache = new SettingsCache("secure");
|
||||||
|
|
||||||
|
// The count of how many known (handled by SettingsProvider)
|
||||||
|
// database mutations are currently being handled. Used by
|
||||||
|
// sFileObserver to not reload the database when it's ourselves
|
||||||
|
// modifying it.
|
||||||
|
private static final AtomicInteger sKnownMutationsInFlight = new AtomicInteger(0);
|
||||||
|
|
||||||
// Over this size we don't reject loading or saving settings but
|
// Over this size we don't reject loading or saving settings but
|
||||||
// we do consider them broken/malicious and don't keep them in
|
// we do consider them broken/malicious and don't keep them in
|
||||||
@@ -67,6 +76,10 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
|
|
||||||
private static final Bundle NULL_SETTING = Bundle.forPair("value", null);
|
private static final Bundle NULL_SETTING = Bundle.forPair("value", null);
|
||||||
|
|
||||||
|
// Used as a sentinel value in an instance equality test when we
|
||||||
|
// want to cache the existence of a key, but not store its value.
|
||||||
|
private static final Bundle TOO_LARGE_TO_CACHE_MARKER = Bundle.forPair("_dummy", null);
|
||||||
|
|
||||||
protected DatabaseHelper mOpenHelper;
|
protected DatabaseHelper mOpenHelper;
|
||||||
private BackupManager mBackupManager;
|
private BackupManager mBackupManager;
|
||||||
|
|
||||||
@@ -201,6 +214,43 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileObserver for external modifications to the database file.
|
||||||
|
// Note that this is for platform developers only with
|
||||||
|
// userdebug/eng builds who should be able to tinker with the
|
||||||
|
// sqlite database out from under the SettingsProvider, which is
|
||||||
|
// normally the exclusive owner of the database. But we keep this
|
||||||
|
// enabled all the time to minimize development-vs-user
|
||||||
|
// differences in testing.
|
||||||
|
private static SettingsFileObserver sObserverInstance;
|
||||||
|
private class SettingsFileObserver extends FileObserver {
|
||||||
|
private final AtomicBoolean mIsDirty = new AtomicBoolean(false);
|
||||||
|
private final String mPath;
|
||||||
|
|
||||||
|
public SettingsFileObserver(String path) {
|
||||||
|
super(path, FileObserver.CLOSE_WRITE |
|
||||||
|
FileObserver.CREATE | FileObserver.DELETE |
|
||||||
|
FileObserver.MOVED_TO | FileObserver.MODIFY);
|
||||||
|
mPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onEvent(int event, String path) {
|
||||||
|
int modsInFlight = sKnownMutationsInFlight.get();
|
||||||
|
if (modsInFlight > 0) {
|
||||||
|
// our own modification.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "external modification to " + mPath + "; event=" + event);
|
||||||
|
if (!mIsDirty.compareAndSet(false, true)) {
|
||||||
|
// already handled. (we get a few update events
|
||||||
|
// during an sqlite write)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "updating our caches for " + mPath);
|
||||||
|
fullyPopulateCaches();
|
||||||
|
mIsDirty.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreate() {
|
public boolean onCreate() {
|
||||||
mOpenHelper = new DatabaseHelper(getContext());
|
mOpenHelper = new DatabaseHelper(getContext());
|
||||||
@@ -210,9 +260,65 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch for external modifications to the database file,
|
||||||
|
// keeping our cache in sync.
|
||||||
|
// It's kinda lame to call mOpenHelper.getReadableDatabase()
|
||||||
|
// during onCreate(), but since ensureAndroidIdIsSet has
|
||||||
|
// already done it above and initialized/upgraded the
|
||||||
|
// database, might as well just use it...
|
||||||
|
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
|
||||||
|
sObserverInstance = new SettingsFileObserver(db.getPath());
|
||||||
|
sObserverInstance.startWatching();
|
||||||
|
startAsyncCachePopulation();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startAsyncCachePopulation() {
|
||||||
|
new Thread("populate-settings-caches") {
|
||||||
|
public void run() {
|
||||||
|
fullyPopulateCaches();
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fullyPopulateCaches() {
|
||||||
|
fullyPopulateCache("secure", sSecureCache);
|
||||||
|
fullyPopulateCache("system", sSystemCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slurp all values (if sane in number & size) into cache.
|
||||||
|
private void fullyPopulateCache(String table, SettingsCache cache) {
|
||||||
|
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
|
||||||
|
Cursor c = db.query(
|
||||||
|
table,
|
||||||
|
new String[] { Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE },
|
||||||
|
null, null, null, null, null,
|
||||||
|
"" + (MAX_CACHE_ENTRIES + 1) /* limit */);
|
||||||
|
try {
|
||||||
|
synchronized (cache) {
|
||||||
|
cache.clear();
|
||||||
|
cache.setFullyMatchesDisk(true); // optimistic
|
||||||
|
int rows = 0;
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
rows++;
|
||||||
|
String name = c.getString(0);
|
||||||
|
String value = c.getString(1);
|
||||||
|
cache.populate(name, value);
|
||||||
|
}
|
||||||
|
if (rows > MAX_CACHE_ENTRIES) {
|
||||||
|
// Somewhat redundant, as removeEldestEntry() will
|
||||||
|
// have already done this, but to be explicit:
|
||||||
|
cache.setFullyMatchesDisk(false);
|
||||||
|
Log.d(TAG, "row count exceeds max cache entries for table " + table);
|
||||||
|
}
|
||||||
|
Log.d(TAG, "cache for settings table '" + table + "' fullycached=" +
|
||||||
|
cache.fullyMatchesDisk());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
c.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean ensureAndroidIdIsSet() {
|
private boolean ensureAndroidIdIsSet() {
|
||||||
final Cursor c = query(Settings.Secure.CONTENT_URI,
|
final Cursor c = query(Settings.Secure.CONTENT_URI,
|
||||||
new String[] { Settings.NameValueTable.VALUE },
|
new String[] { Settings.NameValueTable.VALUE },
|
||||||
@@ -262,7 +368,19 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
private Bundle lookupValue(String table, SettingsCache cache, String key) {
|
private Bundle lookupValue(String table, SettingsCache cache, String key) {
|
||||||
synchronized (cache) {
|
synchronized (cache) {
|
||||||
if (cache.containsKey(key)) {
|
if (cache.containsKey(key)) {
|
||||||
return cache.get(key);
|
Bundle value = cache.get(key);
|
||||||
|
if (value != TOO_LARGE_TO_CACHE_MARKER) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
// else we fall through and read the value from disk
|
||||||
|
} else if (cache.fullyMatchesDisk()) {
|
||||||
|
// Fast path (very common). Don't even try touch disk
|
||||||
|
// if we know we've slurped it all in. Trying to
|
||||||
|
// touch the disk would mean waiting for yaffs2 to
|
||||||
|
// give us access, which could takes hundreds of
|
||||||
|
// milliseconds. And we're very likely being called
|
||||||
|
// from somebody's UI thread...
|
||||||
|
return NULL_SETTING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +456,7 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
checkWritePermissions(args);
|
checkWritePermissions(args);
|
||||||
SettingsCache cache = SettingsCache.forTable(args.table);
|
SettingsCache cache = SettingsCache.forTable(args.table);
|
||||||
|
|
||||||
|
sKnownMutationsInFlight.incrementAndGet();
|
||||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
try {
|
try {
|
||||||
@@ -350,6 +469,7 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
|
sKnownMutationsInFlight.decrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendNotify(uri);
|
sendNotify(uri);
|
||||||
@@ -449,8 +569,10 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
return Uri.withAppendedPath(url, name);
|
return Uri.withAppendedPath(url, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sKnownMutationsInFlight.incrementAndGet();
|
||||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
||||||
final long rowId = db.insert(args.table, null, initialValues);
|
final long rowId = db.insert(args.table, null, initialValues);
|
||||||
|
sKnownMutationsInFlight.decrementAndGet();
|
||||||
if (rowId <= 0) return null;
|
if (rowId <= 0) return null;
|
||||||
|
|
||||||
SettingsCache.populate(cache, initialValues); // before we notify
|
SettingsCache.populate(cache, initialValues); // before we notify
|
||||||
@@ -471,12 +593,15 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
}
|
}
|
||||||
checkWritePermissions(args);
|
checkWritePermissions(args);
|
||||||
|
|
||||||
|
sKnownMutationsInFlight.incrementAndGet();
|
||||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
||||||
int count = db.delete(args.table, args.where, args.args);
|
int count = db.delete(args.table, args.where, args.args);
|
||||||
|
sKnownMutationsInFlight.decrementAndGet();
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
SettingsCache.wipe(args.table); // before we notify
|
SettingsCache.wipe(args.table); // before we notify
|
||||||
sendNotify(url);
|
sendNotify(url);
|
||||||
}
|
}
|
||||||
|
startAsyncCachePopulation();
|
||||||
if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted");
|
if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted");
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -489,12 +614,15 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
}
|
}
|
||||||
checkWritePermissions(args);
|
checkWritePermissions(args);
|
||||||
|
|
||||||
|
sKnownMutationsInFlight.incrementAndGet();
|
||||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
||||||
|
sKnownMutationsInFlight.decrementAndGet();
|
||||||
int count = db.update(args.table, initialValues, args.where, args.args);
|
int count = db.update(args.table, initialValues, args.where, args.args);
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
SettingsCache.wipe(args.table); // before we notify
|
SettingsCache.wipe(args.table); // before we notify
|
||||||
sendNotify(url);
|
sendNotify(url);
|
||||||
}
|
}
|
||||||
|
startAsyncCachePopulation();
|
||||||
if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues);
|
if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -506,12 +634,12 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
* When a client attempts to openFile the default ringtone or
|
* When a client attempts to openFile the default ringtone or
|
||||||
* notification setting Uri, we will proxy the call to the current
|
* notification setting Uri, we will proxy the call to the current
|
||||||
* default ringtone's Uri (if it is in the DRM or media provider).
|
* default ringtone's Uri (if it is in the DRM or media provider).
|
||||||
*/
|
*/
|
||||||
int ringtoneType = RingtoneManager.getDefaultType(uri);
|
int ringtoneType = RingtoneManager.getDefaultType(uri);
|
||||||
// Above call returns -1 if the Uri doesn't match a default type
|
// Above call returns -1 if the Uri doesn't match a default type
|
||||||
if (ringtoneType != -1) {
|
if (ringtoneType != -1) {
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
|
|
||||||
// Get the current value for the default sound
|
// Get the current value for the default sound
|
||||||
Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
|
Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
|
||||||
|
|
||||||
@@ -531,7 +659,7 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
throw new FileNotFoundException(e.getMessage());
|
throw new FileNotFoundException(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return context.getContentResolver().openFileDescriptor(soundUri, mode);
|
return context.getContentResolver().openFileDescriptor(soundUri, mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -607,13 +735,38 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
*/
|
*/
|
||||||
private static final class SettingsCache extends LinkedHashMap<String, Bundle> {
|
private static final class SettingsCache extends LinkedHashMap<String, Bundle> {
|
||||||
|
|
||||||
public SettingsCache() {
|
private final String mCacheName;
|
||||||
|
private boolean mCacheFullyMatchesDisk = false; // has the whole database slurped.
|
||||||
|
|
||||||
|
public SettingsCache(String name) {
|
||||||
super(MAX_CACHE_ENTRIES, 0.75f /* load factor */, true /* access ordered */);
|
super(MAX_CACHE_ENTRIES, 0.75f /* load factor */, true /* access ordered */);
|
||||||
|
mCacheName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the whole database table slurped into this cache?
|
||||||
|
*/
|
||||||
|
public boolean fullyMatchesDisk() {
|
||||||
|
synchronized (this) {
|
||||||
|
return mCacheFullyMatchesDisk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFullyMatchesDisk(boolean value) {
|
||||||
|
synchronized (this) {
|
||||||
|
mCacheFullyMatchesDisk = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean removeEldestEntry(Map.Entry eldest) {
|
protected boolean removeEldestEntry(Map.Entry eldest) {
|
||||||
return size() > MAX_CACHE_ENTRIES;
|
if (size() <= MAX_CACHE_ENTRIES) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
mCacheFullyMatchesDisk = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -658,11 +811,15 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String value = contentValues.getAsString(Settings.NameValueTable.VALUE);
|
String value = contentValues.getAsString(Settings.NameValueTable.VALUE);
|
||||||
synchronized (cache) {
|
cache.populate(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populate(String name, String value) {
|
||||||
|
synchronized (this) {
|
||||||
if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
|
if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
|
||||||
cache.put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value));
|
put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value));
|
||||||
} else {
|
} else {
|
||||||
cache.remove(name);
|
put(name, TOO_LARGE_TO_CACHE_MARKER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -678,6 +835,7 @@ public class SettingsProvider extends ContentProvider {
|
|||||||
}
|
}
|
||||||
synchronized (cache) {
|
synchronized (cache) {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
|
cache.mCacheFullyMatchesDisk = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user