diff options
author | George Mount <mount@google.com> | 2013-03-07 21:54:29 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2013-03-07 21:54:30 +0000 |
commit | e988eba768db85bb81b335c0c1db5aad6988480d (patch) | |
tree | 24b53c6818fb070c19a1169c5ceb7539b2579cbf /src/com | |
parent | 9603a0c884208a0dc9ef906043387a1758a97656 (diff) | |
parent | 07d00fa7535566d401ad4cf5002af626f907a514 (diff) | |
download | android_packages_apps_Snap-e988eba768db85bb81b335c0c1db5aad6988480d.tar.gz android_packages_apps_Snap-e988eba768db85bb81b335c0c1db5aad6988480d.tar.bz2 android_packages_apps_Snap-e988eba768db85bb81b335c0c1db5aad6988480d.zip |
Merge "Add batch operations to PhotoProvider" into gb-ub-photos-bryce
Diffstat (limited to 'src/com')
-rw-r--r-- | src/com/android/photos/data/NotificationWatcher.java | 14 | ||||
-rw-r--r-- | src/com/android/photos/data/PhotoProvider.java | 126 | ||||
-rw-r--r-- | src/com/android/photos/data/SQLiteContentProvider.java | 264 |
3 files changed, 320 insertions, 84 deletions
diff --git a/src/com/android/photos/data/NotificationWatcher.java b/src/com/android/photos/data/NotificationWatcher.java index 8cf0e3c8f..9041c236f 100644 --- a/src/com/android/photos/data/NotificationWatcher.java +++ b/src/com/android/photos/data/NotificationWatcher.java @@ -19,8 +19,7 @@ import android.net.Uri; import com.android.photos.data.PhotoProvider.ChangeNotification; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; /** * Used for capturing notifications from PhotoProvider without relying on @@ -28,11 +27,13 @@ import java.util.Set; * ContentObservers, so PhotoProvider allows this alternative for testing. */ public class NotificationWatcher implements ChangeNotification { - private Set<Uri> mUris = new HashSet<Uri>(); + private ArrayList<Uri> mUris = new ArrayList<Uri>(); + private boolean mSyncToNetwork = false; @Override - public void notifyChange(Uri uri) { + public void notifyChange(Uri uri, boolean syncToNetwork) { mUris.add(uri); + mSyncToNetwork = mSyncToNetwork || syncToNetwork; } public boolean isNotified(Uri uri) { @@ -43,7 +44,12 @@ public class NotificationWatcher implements ChangeNotification { return mUris.size(); } + public boolean syncToNetwork() { + return mSyncToNetwork; + } + public void reset() { mUris.clear(); + mSyncToNetwork = false; } } diff --git a/src/com/android/photos/data/PhotoProvider.java b/src/com/android/photos/data/PhotoProvider.java index 7d751bf95..cecfe5ea4 100644 --- a/src/com/android/photos/data/PhotoProvider.java +++ b/src/com/android/photos/data/PhotoProvider.java @@ -15,9 +15,10 @@ */ package com.android.photos.data; -import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; +import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.DatabaseUtils; @@ -29,7 +30,6 @@ import android.net.Uri; import android.os.CancellationSignal; import android.provider.BaseColumns; -import java.util.ArrayList; import java.util.List; /** @@ -46,7 +46,7 @@ import java.util.List; * in the selection. The selection and selectionArgs are not used when updating * metadata. If the metadata values are null, the row will be deleted. */ -public class PhotoProvider extends ContentProvider { +public class PhotoProvider extends SQLiteContentProvider { @SuppressWarnings("unused") private static final String TAG = PhotoProvider.class.getSimpleName(); @@ -58,7 +58,7 @@ public class PhotoProvider extends ContentProvider { // Used to allow mocking out the change notification because // MockContextResolver disallows system-wide notification. public static interface ChangeNotification { - void notifyChange(Uri uri); + void notifyChange(Uri uri, boolean syncToNetwork); } /** @@ -272,7 +272,6 @@ public class PhotoProvider extends ContentProvider { }; protected ChangeNotification mNotifier = null; - private SQLiteOpenHelper mOpenHelper; protected static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); protected static final int MATCH_PHOTO = 1; @@ -302,23 +301,14 @@ public class PhotoProvider extends ContentProvider { } @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { + public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, + boolean callerIsSyncAdapter) { int match = matchUri(uri); selection = addIdToSelection(match, selection); selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); - List<Uri> changeUris = new ArrayList<Uri>(); int deleted = 0; - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - db.beginTransaction(); - try { - deleted = deleteCascade(db, match, selection, selectionArgs, changeUris, uri); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - for (Uri changeUri : changeUris) { - notifyChanges(changeUri); - } + SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); + deleted = deleteCascade(db, match, selection, selectionArgs, uri); return deleted; } @@ -334,39 +324,22 @@ public class PhotoProvider extends ContentProvider { } @Override - public Uri insert(Uri uri, ContentValues values) { + public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { int match = matchUri(uri); validateMatchTable(match); String table = getTableFromMatch(match, uri); - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); Uri insertedUri = null; - db.beginTransaction(); - try { - long id = db.insert(table, null, values); - if (id != -1) { - // uri already matches the table. - insertedUri = ContentUris.withAppendedId(uri, id); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); + long id = db.insert(table, null, values); + if (id != -1) { + // uri already matches the table. + insertedUri = ContentUris.withAppendedId(uri, id); + postNotifyUri(insertedUri); } - notifyChanges(insertedUri); return insertedUri; } @Override - public boolean onCreate() { - mOpenHelper = createDatabaseHelper(); - return true; - } - - @Override - public void shutdown() { - getDatabaseHelper().close(); - } - - @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return query(uri, projection, selection, selectionArgs, sortOrder, null); @@ -379,31 +352,26 @@ public class PhotoProvider extends ContentProvider { selection = addIdToSelection(match, selection); selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); String table = getTableFromMatch(match, uri); - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + SQLiteDatabase db = getDatabaseHelper().getReadableDatabase(); return db.query(false, table, projection, selection, selectionArgs, null, null, sortOrder, null, cancellationSignal); } @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + public int updateInTransaction(Uri uri, ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter) { int match = matchUri(uri); int rowsUpdated = 0; - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - db.beginTransaction(); - try { - if (match == MATCH_METADATA) { - rowsUpdated = modifyMetadata(db, values); - } else { - selection = addIdToSelection(match, selection); - selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); - String table = getTableFromMatch(match, uri); - rowsUpdated = db.update(table, values, selection, selectionArgs); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); + SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); + if (match == MATCH_METADATA) { + rowsUpdated = modifyMetadata(db, values); + } else { + selection = addIdToSelection(match, selection); + selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); + String table = getTableFromMatch(match, uri); + rowsUpdated = db.update(table, values, selection, selectionArgs); } - notifyChanges(uri); + postNotifyUri(uri); return rowsUpdated; } @@ -472,12 +440,9 @@ public class PhotoProvider extends ContentProvider { return table; } - protected final SQLiteOpenHelper getDatabaseHelper() { - return mOpenHelper; - } - - protected SQLiteOpenHelper createDatabaseHelper() { - return new PhotoDatabase(getContext(), DB_NAME); + @Override + public SQLiteOpenHelper getDatabaseHelper(Context context) { + return new PhotoDatabase(context, DB_NAME); } private int modifyMetadata(SQLiteDatabase db, ContentValues values) { @@ -505,11 +470,12 @@ public class PhotoProvider extends ContentProvider { return match; } - protected void notifyChanges(Uri uri) { + @Override + protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) { if (mNotifier != null) { - mNotifier.notifyChange(uri); + mNotifier.notifyChange(uri, syncToNetwork); } else { - getContext().getContentResolver().notifyChange(uri, null, false); + resolver.notifyChange(uri, null, syncToNetwork); } } @@ -523,44 +489,44 @@ public class PhotoProvider extends ContentProvider { return matchColumn + IN + NESTED_SELECT_START + query + NESTED_SELECT_END; } - protected static int deleteCascade(SQLiteDatabase db, int match, String selection, - String[] selectionArgs, List<Uri> changeUris, Uri uri) { + protected int deleteCascade(SQLiteDatabase db, int match, String selection, + String[] selectionArgs, Uri uri) { switch (match) { case MATCH_PHOTO: case MATCH_PHOTO_ID: { - deleteCascadeMetadata(db, selection, selectionArgs, changeUris); + deleteCascadeMetadata(db, selection, selectionArgs); break; } case MATCH_ALBUM: case MATCH_ALBUM_ID: { - deleteCascadePhotos(db, selection, selectionArgs, changeUris); + deleteCascadePhotos(db, selection, selectionArgs); break; } } String table = getTableFromMatch(match, uri); int deleted = db.delete(table, selection, selectionArgs); if (deleted > 0) { - changeUris.add(uri); + postNotifyUri(uri); } return deleted; } - private static void deleteCascadePhotos(SQLiteDatabase db, String albumSelect, - String[] selectArgs, List<Uri> changeUris) { + private void deleteCascadePhotos(SQLiteDatabase db, String albumSelect, + String[] selectArgs) { String photoWhere = nestWhere(Photos.ALBUM_ID, Albums.TABLE, albumSelect); - deleteCascadeMetadata(db, photoWhere, selectArgs, changeUris); + deleteCascadeMetadata(db, photoWhere, selectArgs); int deleted = db.delete(Photos.TABLE, photoWhere, selectArgs); if (deleted > 0) { - changeUris.add(Photos.CONTENT_URI); + postNotifyUri(Photos.CONTENT_URI); } } - private static void deleteCascadeMetadata(SQLiteDatabase db, String photosSelect, - String[] selectArgs, List<Uri> changeUris) { + private void deleteCascadeMetadata(SQLiteDatabase db, String photosSelect, + String[] selectArgs) { String metadataWhere = nestWhere(Metadata.PHOTO_ID, Photos.TABLE, photosSelect); int deleted = db.delete(Metadata.TABLE, metadataWhere, selectArgs); if (deleted > 0) { - changeUris.add(Metadata.CONTENT_URI); + postNotifyUri(Metadata.CONTENT_URI); } } diff --git a/src/com/android/photos/data/SQLiteContentProvider.java b/src/com/android/photos/data/SQLiteContentProvider.java new file mode 100644 index 000000000..ecd868b52 --- /dev/null +++ b/src/com/android/photos/data/SQLiteContentProvider.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.photos.data; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * General purpose {@link ContentProvider} base class that uses SQLiteDatabase + * for storage. + */ +public abstract class SQLiteContentProvider extends ContentProvider { + + @SuppressWarnings("unused") + private static final String TAG = "SQLiteContentProvider"; + + private SQLiteOpenHelper mOpenHelper; + private Set<Uri> mChangedUris; + + private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>(); + private static final int SLEEP_AFTER_YIELD_DELAY = 4000; + + /** + * Maximum number of operations allowed in a batch between yield points. + */ + private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500; + + @Override + public boolean onCreate() { + Context context = getContext(); + mOpenHelper = getDatabaseHelper(context); + mChangedUris = new HashSet<Uri>(); + return true; + } + + @Override + public void shutdown() { + getDatabaseHelper().close(); + } + + /** + * Returns a {@link SQLiteOpenHelper} that can open the database. + */ + public abstract SQLiteOpenHelper getDatabaseHelper(Context context); + + /** + * The equivalent of the {@link #insert} method, but invoked within a + * transaction. + */ + public abstract Uri insertInTransaction(Uri uri, ContentValues values, + boolean callerIsSyncAdapter); + + /** + * The equivalent of the {@link #update} method, but invoked within a + * transaction. + */ + public abstract int updateInTransaction(Uri uri, ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter); + + /** + * The equivalent of the {@link #delete} method, but invoked within a + * transaction. + */ + public abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, + boolean callerIsSyncAdapter); + + /** + * Call this to add a URI to the list of URIs to be notified when the + * transaction is committed. + */ + protected void postNotifyUri(Uri uri) { + synchronized (mChangedUris) { + mChangedUris.add(uri); + } + } + + public boolean isCallerSyncAdapter(Uri uri) { + return false; + } + + public SQLiteOpenHelper getDatabaseHelper() { + return mOpenHelper; + } + + private boolean applyingBatch() { + return mApplyingBatch.get() != null && mApplyingBatch.get(); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + Uri result = null; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + result = insertInTransaction(uri, values, callerIsSyncAdapter); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + result = insertInTransaction(uri, values, callerIsSyncAdapter); + } + return result; + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + int numValues = values.length; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + for (int i = 0; i < numValues; i++) { + @SuppressWarnings("unused") + Uri result = insertInTransaction(uri, values[i], callerIsSyncAdapter); + db.yieldIfContendedSafely(); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + return numValues; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + count = updateInTransaction(uri, values, selection, selectionArgs, + callerIsSyncAdapter); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + count = updateInTransaction(uri, values, selection, selectionArgs, callerIsSyncAdapter); + } + + return count; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter); + } + return count; + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + int ypCount = 0; + int opCount = 0; + boolean callerIsSyncAdapter = false; + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + mApplyingBatch.set(true); + final int numOperations = operations.size(); + final ContentProviderResult[] results = new ContentProviderResult[numOperations]; + for (int i = 0; i < numOperations; i++) { + if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) { + throw new OperationApplicationException( + "Too many content provider operations between yield points. " + + "The maximum number of operations per yield point is " + + MAX_OPERATIONS_PER_YIELD_POINT, ypCount); + } + final ContentProviderOperation operation = operations.get(i); + if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) { + callerIsSyncAdapter = true; + } + if (i > 0 && operation.isYieldAllowed()) { + opCount = 0; + if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) { + ypCount++; + } + } + results[i] = operation.apply(this, results, i); + } + db.setTransactionSuccessful(); + return results; + } finally { + mApplyingBatch.set(false); + db.endTransaction(); + onEndTransaction(callerIsSyncAdapter); + } + } + + protected void onEndTransaction(boolean callerIsSyncAdapter) { + Set<Uri> changed; + synchronized (mChangedUris) { + changed = new HashSet<Uri>(mChangedUris); + mChangedUris.clear(); + } + ContentResolver resolver = getContext().getContentResolver(); + for (Uri uri : changed) { + boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri); + notifyChange(resolver, uri, syncToNetwork); + } + } + + protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) { + resolver.notifyChange(uri, null, syncToNetwork); + } + + protected boolean syncToNetwork(Uri uri) { + return false; + } +}
\ No newline at end of file |