summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/com/android/photos/data/NotificationWatcher.java14
-rw-r--r--src/com/android/photos/data/PhotoProvider.java126
-rw-r--r--src/com/android/photos/data/SQLiteContentProvider.java264
-rw-r--r--tests/src/com/android/photos/data/PhotoProviderTest.java39
4 files changed, 359 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
diff --git a/tests/src/com/android/photos/data/PhotoProviderTest.java b/tests/src/com/android/photos/data/PhotoProviderTest.java
index 47c6e86b2..39abff441 100644
--- a/tests/src/com/android/photos/data/PhotoProviderTest.java
+++ b/tests/src/com/android/photos/data/PhotoProviderTest.java
@@ -15,13 +15,16 @@
*/
package com.android.photos.data;
+import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
+import android.content.OperationApplicationException;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
+import android.os.RemoteException;
import android.provider.BaseColumns;
import android.test.ProviderTestCase2;
@@ -29,6 +32,8 @@ import com.android.photos.data.PhotoProvider.Albums;
import com.android.photos.data.PhotoProvider.Metadata;
import com.android.photos.data.PhotoProvider.Photos;
+import java.util.ArrayList;
+
public class PhotoProviderTest extends ProviderTestCase2<PhotoProvider> {
@SuppressWarnings("unused")
private static final String TAG = PhotoProviderTest.class.getSimpleName();
@@ -317,4 +322,38 @@ public class PhotoProviderTest extends ProviderTestCase2<PhotoProvider> {
mResolver.update(Metadata.CONTENT_URI, values, null, null);
assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
}
+
+ public void testBatchTransaction() throws RemoteException, OperationApplicationException {
+ ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
+ ContentProviderOperation.Builder insert = ContentProviderOperation
+ .newInsert(Photos.CONTENT_URI);
+ insert.withValue(Photos.WIDTH, 200L);
+ insert.withValue(Photos.HEIGHT, 100L);
+ insert.withValue(Photos.DATE_TAKEN, System.currentTimeMillis());
+ insert.withValue(Photos.ALBUM_ID, 1000L);
+ insert.withValue(Photos.MIME_TYPE, "image/jpg");
+ insert.withValue(Photos.ACCOUNT_ID, 1L);
+ operations.add(insert.build());
+ ContentProviderOperation.Builder update = ContentProviderOperation.newUpdate(Photos.CONTENT_URI);
+ update.withValue(Photos.DATE_MODIFIED, System.currentTimeMillis());
+ String[] whereArgs = {
+ "100",
+ };
+ String where = Photos.WIDTH + " = ?";
+ update.withSelection(where, whereArgs);
+ operations.add(update.build());
+ ContentProviderOperation.Builder delete = ContentProviderOperation
+ .newDelete(Photos.CONTENT_URI);
+ delete.withSelection(where, whereArgs);
+ operations.add(delete.build());
+ mResolver.applyBatch(PhotoProvider.AUTHORITY, operations);
+ assertEquals(3, mNotifications.notificationCount());
+ SQLiteDatabase db = mDBHelper.getReadableDatabase();
+ long id = PhotoDatabaseUtils.queryPhotoIdFromAlbumId(db, 1000L);
+ Uri uri = ContentUris.withAppendedId(Photos.CONTENT_URI, id);
+ assertTrue(mNotifications.isNotified(uri));
+ assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
+ assertTrue(mNotifications.isNotified(Photos.CONTENT_URI));
+ }
+
}