OSDN Git Service

Add batch operations to PhotoProvider
authorGeorge Mount <mount@google.com>
Thu, 7 Mar 2013 17:02:26 +0000 (09:02 -0800)
committerGeorge Mount <mount@google.com>
Thu, 7 Mar 2013 21:40:31 +0000 (13:40 -0800)
Change-Id: I36b2f0305e2ef7502df7b896c8baedc86519ac52

src/com/android/photos/data/NotificationWatcher.java
src/com/android/photos/data/PhotoProvider.java
src/com/android/photos/data/SQLiteContentProvider.java [new file with mode: 0644]
tests/src/com/android/photos/data/PhotoProviderTest.java

index 8cf0e3c..9041c23 100644 (file)
@@ -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;
     }
 }
index 7d751bf..cecfe5e 100644 (file)
  */
 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 (file)
index 0000000..ecd868b
--- /dev/null
@@ -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
index 47c6e86..39abff4 100644 (file)
  */
 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));
+    }
+
 }