Update PhotoProvider and tests to allow tests to run against GalleryGoogle.apk

Change-Id: I6630e7a5ba0883b887915f63094885a2dc9f025c
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f7734bb..581c761 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -89,4 +89,6 @@
     <dimen name="zoom_font_size">14pt</dimen>
     <dimen name="shutter_offset">-22dp</dimen>
     <dimen name="margin_systemui_offset">6dip</dimen>
+    <dimen name="size_thumbnail">200dip</dimen>
+    <dimen name="size_preview">600dip</dimen>
 </resources>
diff --git a/src/com/android/photos/data/NotificationWatcher.java b/src/com/android/photos/data/NotificationWatcher.java
new file mode 100644
index 0000000..8cf0e3c
--- /dev/null
+++ b/src/com/android/photos/data/NotificationWatcher.java
@@ -0,0 +1,49 @@
+/*
+ * 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.net.Uri;
+
+import com.android.photos.data.PhotoProvider.ChangeNotification;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Used for capturing notifications from PhotoProvider without relying on
+ * ContentResolver. MockContentResolver does not allow sending notification to
+ * ContentObservers, so PhotoProvider allows this alternative for testing.
+ */
+public class NotificationWatcher implements ChangeNotification {
+    private Set<Uri> mUris = new HashSet<Uri>();
+
+    @Override
+    public void notifyChange(Uri uri) {
+        mUris.add(uri);
+    }
+
+    public boolean isNotified(Uri uri) {
+        return mUris.contains(uri);
+    }
+
+    public int notificationCount() {
+        return mUris.size();
+    }
+
+    public void reset() {
+        mUris.clear();
+    }
+}
diff --git a/src/com/android/photos/data/PhotoDatabase.java b/src/com/android/photos/data/PhotoDatabase.java
index 64a857f..35de185 100644
--- a/src/com/android/photos/data/PhotoDatabase.java
+++ b/src/com/android/photos/data/PhotoDatabase.java
@@ -30,7 +30,6 @@
 public class PhotoDatabase extends SQLiteOpenHelper {
     @SuppressWarnings("unused")
     private static final String TAG = PhotoDatabase.class.getSimpleName();
-    static final String DB_NAME = "photo.db";
     static final int DB_VERSION = 1;
 
     private static final String SQL_CREATE_TABLE = "CREATE TABLE ";
@@ -72,8 +71,8 @@
         createTable(db, Metadata.TABLE, CREATE_METADATA);
     }
 
-    public PhotoDatabase(Context context) {
-        super(context, DB_NAME, null, DB_VERSION);
+    public PhotoDatabase(Context context, String dbName) {
+        super(context, dbName, null, DB_VERSION);
     }
 
     @Override
diff --git a/src/com/android/photos/data/PhotoProvider.java b/src/com/android/photos/data/PhotoProvider.java
index eefa373..7b591f8 100644
--- a/src/com/android/photos/data/PhotoProvider.java
+++ b/src/com/android/photos/data/PhotoProvider.java
@@ -16,18 +16,19 @@
 package com.android.photos.data;
 
 import android.content.ContentProvider;
-import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteStatement;
+import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.os.CancellationSignal;
 import android.provider.BaseColumns;
 
+import com.google.android.gms.common.util.VisibleForTesting;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -48,10 +49,18 @@
 public class PhotoProvider extends ContentProvider {
     @SuppressWarnings("unused")
     private static final String TAG = PhotoProvider.class.getSimpleName();
-    static final String AUTHORITY = "com.android.gallery3d.photoprovider";
+
+    protected static final String DB_NAME = "photo.db";
+    public static final String AUTHORITY = PhotoProviderAuthority.AUTHORITY;
     static final Uri BASE_CONTENT_URI = new Uri.Builder().scheme("content").authority(AUTHORITY)
             .build();
 
+    // Used to allow mocking out the change notification because
+    // MockContextResolver disallows system-wide notification.
+    public static interface ChangeNotification {
+        void notifyChange(Uri uri);
+    }
+
     /**
      * Contains columns that can be accessed via PHOTOS_CONTENT_URI.
      */
@@ -185,7 +194,7 @@
         /**
          * Foreign key to the photos._id. Long value.
          */
-        public static final String PHOTO_ID = "photos_id";
+        public static final String PHOTO_ID = "photo_id";
         /**
          * One of IMAGE_TYPE_* values.
          */
@@ -221,6 +230,11 @@
         Photos.MIME_TYPE,
     };
 
+    private static final String[] BASE_COLUMNS_ID = {
+        BaseColumns._ID,
+    };
+
+    protected ChangeNotification mNotifier = null;
     private SQLiteOpenHelper mOpenHelper;
     protected static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 
@@ -294,6 +308,11 @@
     }
 
     @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);
@@ -334,6 +353,11 @@
         return rowsUpdated;
     }
 
+    @VisibleForTesting
+    public void setMockNotification(ChangeNotification notification) {
+        mNotifier = notification;
+    }
+
     protected static String addIdToSelection(int match, String selection) {
         String where;
         switch (match) {
@@ -400,7 +424,7 @@
     }
 
     protected SQLiteOpenHelper createDatabaseHelper() {
-        return new PhotoDatabase(getContext());
+        return new PhotoDatabase(getContext(), DB_NAME);
     }
 
     private int modifyMetadata(SQLiteDatabase db, ContentValues values) {
@@ -433,30 +457,21 @@
     }
 
     protected void notifyChanges(Uri uri) {
-        ContentResolver resolver = getContext().getContentResolver();
-        resolver.notifyChange(uri, null, false);
+        if (mNotifier != null) {
+            mNotifier.notifyChange(uri);
+        } else {
+            getContext().getContentResolver().notifyChange(uri, null, false);
+        }
     }
 
     protected static IllegalArgumentException unknownUri(Uri uri) {
         return new IllegalArgumentException("Unknown Uri format: " + uri);
     }
 
-    protected static String nestSql(String base, String columnMatch, String nested) {
-        StringBuilder sql = new StringBuilder(base);
-        sql.append(WHERE);
-        sql.append(columnMatch);
-        sql.append(IN);
-        sql.append(NESTED_SELECT_START);
-        sql.append(nested);
-        sql.append(NESTED_SELECT_END);
-        return sql.toString();
-    }
-
-    protected static String addWhere(String base, String where) {
-        if (where == null || where.isEmpty()) {
-            return base;
-        }
-        return base + WHERE + where;
+    protected static String nestWhere(String matchColumn, String table, String nestedWhere) {
+        String query = SQLiteQueryBuilder.buildQueryString(false, table, BASE_COLUMNS_ID,
+                nestedWhere, null, null, null, null);
+        return matchColumn + IN + NESTED_SELECT_START + query + NESTED_SELECT_END;
     }
 
     protected static int deleteCascade(SQLiteDatabase db, int match, String selection,
@@ -464,38 +479,28 @@
         switch (match) {
             case MATCH_PHOTO:
             case MATCH_PHOTO_ID: {
-                String selectPhotoIdsSql = addWhere(SELECT_PHOTO_ID, selection);
-                deleteCascadeMetadata(db, selectPhotoIdsSql, selectionArgs, changeUris);
+                deleteCascadeMetadata(db, selection, selectionArgs, changeUris);
                 break;
             }
             case MATCH_ALBUM:
             case MATCH_ALBUM_ID: {
-                String selectAlbumIdSql = addWhere(SELECT_ALBUM_ID, selection);
-                deleteCascadePhotos(db, selectAlbumIdSql, selectionArgs, changeUris);
+                deleteCascadePhotos(db, selection, selectionArgs, changeUris);
                 break;
             }
         }
         String table = getTableFromMatch(match, uri);
-        changeUris.add(uri);
-        return db.delete(table, selection, selectionArgs);
-    }
-
-    protected static void execSql(SQLiteDatabase db, String sql, String[] args) {
-        if (args == null) {
-            db.execSQL(sql);
-        } else {
-            db.execSQL(sql, args);
+        int deleted = db.delete(table, selection, selectionArgs);
+        if (deleted > 0) {
+            changeUris.add(uri);
         }
+        return deleted;
     }
 
     private static void deleteCascadePhotos(SQLiteDatabase db, String albumSelect,
             String[] selectArgs, List<Uri> changeUris) {
-        String selectPhotoIdSql = nestSql(SELECT_PHOTO_ID, Photos.ALBUM_ID, albumSelect);
-        deleteCascadeMetadata(db, selectPhotoIdSql, selectArgs, changeUris);
-        String deletePhotoSql = nestSql(DELETE_PHOTOS, Photos.ALBUM_ID, albumSelect);
-        SQLiteStatement statement = db.compileStatement(deletePhotoSql);
-        statement.bindAllArgsAsStrings(selectArgs);
-        int deleted = statement.executeUpdateDelete();
+        String photoWhere = nestWhere(Photos.ALBUM_ID, Albums.TABLE, albumSelect);
+        deleteCascadeMetadata(db, photoWhere, selectArgs, changeUris);
+        int deleted = db.delete(Photos.TABLE, photoWhere, selectArgs);
         if (deleted > 0) {
             changeUris.add(Photos.CONTENT_URI);
         }
@@ -503,10 +508,8 @@
 
     private static void deleteCascadeMetadata(SQLiteDatabase db, String photosSelect,
             String[] selectArgs, List<Uri> changeUris) {
-        String deleteMetadataSql = nestSql(DELETE_METADATA, Metadata.PHOTO_ID, photosSelect);
-        SQLiteStatement statement = db.compileStatement(deleteMetadataSql);
-        statement.bindAllArgsAsStrings(selectArgs);
-        int deleted = statement.executeUpdateDelete();
+        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);
         }
diff --git a/src_pd/com/android/photos/data/PhotoProviderAuthority.java b/src_pd/com/android/photos/data/PhotoProviderAuthority.java
new file mode 100644
index 0000000..0ac76cb
--- /dev/null
+++ b/src_pd/com/android/photos/data/PhotoProviderAuthority.java
@@ -0,0 +1,21 @@
+/*
+ * 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;
+
+interface PhotoProviderAuthority {
+    public static final String AUTHORITY = "com.android.gallery3d.photoprovider";
+}
diff --git a/tests/src/com/android/photos/data/PhotoDatabaseTest.java b/tests/src/com/android/photos/data/PhotoDatabaseTest.java
index 48e79d4..d8c5e42 100644
--- a/tests/src/com/android/photos/data/PhotoDatabaseTest.java
+++ b/tests/src/com/android/photos/data/PhotoDatabaseTest.java
@@ -30,29 +30,32 @@
 public class PhotoDatabaseTest extends InstrumentationTestCase {
 
     private PhotoDatabase mDBHelper;
+    private static final String DB_NAME = "dummy.db";
 
     @Override
-    protected void setUp() {
+    protected void setUp() throws Exception {
+        super.setUp();
         Context context = getInstrumentation().getTargetContext();
-        mDBHelper = new PhotoDatabase(context);
+        context.deleteDatabase(DB_NAME);
+        mDBHelper = new PhotoDatabase(context, DB_NAME);
     }
 
     @Override
-    protected void tearDown() {
+    protected void tearDown() throws Exception {
         mDBHelper.close();
+        mDBHelper = null;
+        Context context = getInstrumentation().getTargetContext();
+        context.deleteDatabase(DB_NAME);
+        super.tearDown();
     }
 
     public void testCreateDatabase() throws IOException {
         Context context = getInstrumentation().getTargetContext();
-        File dbFile = context.getDatabasePath(PhotoDatabase.DB_NAME);
-        if (dbFile.exists()) {
-            dbFile.delete();
-        }
+        File dbFile = context.getDatabasePath(DB_NAME);
         SQLiteDatabase db = getReadableDB();
         db.beginTransaction();
         db.endTransaction();
         assertTrue(dbFile.exists());
-        dbFile.delete();
     }
 
     public void testTables() {
diff --git a/tests/src/com/android/photos/data/PhotoDatabaseUtils.java b/tests/src/com/android/photos/data/PhotoDatabaseUtils.java
index 6fd73e1..73a6c78 100644
--- a/tests/src/com/android/photos/data/PhotoDatabaseUtils.java
+++ b/tests/src/com/android/photos/data/PhotoDatabaseUtils.java
@@ -108,10 +108,4 @@
         values.put(Metadata.VALUE, value);
         return db.insert(Metadata.TABLE, null, values) != -1;
     }
-
-    public static void deleteAllContent(SQLiteDatabase db) {
-        db.delete(Metadata.TABLE, null, null);
-        db.delete(Photos.TABLE, null, null);
-        db.delete(Albums.TABLE, null, null);
-    }
 }
diff --git a/tests/src/com/android/photos/data/PhotoProviderTest.java b/tests/src/com/android/photos/data/PhotoProviderTest.java
index ad913b0..525abec 100644
--- a/tests/src/com/android/photos/data/PhotoProviderTest.java
+++ b/tests/src/com/android/photos/data/PhotoProviderTest.java
@@ -18,21 +18,18 @@
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
-import android.content.Context;
-import android.database.ContentObserver;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
 import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
 import android.provider.BaseColumns;
-import android.test.InstrumentationTestCase;
+import android.test.ProviderTestCase2;
 
 import com.android.photos.data.PhotoProvider.Albums;
 import com.android.photos.data.PhotoProvider.Metadata;
 import com.android.photos.data.PhotoProvider.Photos;
 
-public class PhotoProviderTest extends InstrumentationTestCase {
+public class PhotoProviderTest extends ProviderTestCase2<PhotoProvider> {
     @SuppressWarnings("unused")
     private static final String TAG = PhotoProviderTest.class.getSimpleName();
 
@@ -51,87 +48,25 @@
     private static final String WHERE_METADATA = Metadata.PHOTO_ID + " = ? AND " + Metadata.KEY
             + " = ?";
 
-    private static final long WAIT_FOR_CHANGE_MILLIS = 200;
-
     private long mAlbumId;
     private long mPhotoId;
     private long mMetadataId;
 
-    private PhotoDatabase mDBHelper;
+    private SQLiteOpenHelper mDBHelper;
     private ContentResolver mResolver;
+    private NotificationWatcher mNotifications = new NotificationWatcher();
 
-    private static class WatchContentObserverThread extends Thread {
-        private WatchContentObserver mObserver;
-        private Looper mLooper;
-
-        @Override
-        public void run() {
-            Looper.prepare();
-            mLooper = Looper.myLooper();
-            WatchContentObserver observer = new WatchContentObserver();
-            synchronized (this) {
-                mObserver = observer;
-                this.notifyAll();
-            }
-            Looper.loop();
-        }
-
-        public void waitForObserver() throws InterruptedException {
-            synchronized (this) {
-                while (mObserver == null) {
-                    this.wait();
-                }
-            }
-        }
-
-        public WatchContentObserver getObserver() {
-            return mObserver;
-        }
-
-        public void stopLooper() {
-            mLooper.quit();
-        }
-    };
-
-    private static class WatchContentObserver extends ContentObserver {
-        private boolean mOnChangeReceived = false;
-        private Uri mUri = null;
-
-        public WatchContentObserver() {
-            super(new Handler());
-        }
-
-        @Override
-        public synchronized void onChange(boolean selfChange, Uri uri) {
-            mOnChangeReceived = true;
-            mUri = uri;
-            notifyAll();
-        }
-
-        @Override
-        public synchronized void onChange(boolean selfChange) {
-            mOnChangeReceived = true;
-            notifyAll();
-        }
-
-        public boolean waitForNotification() {
-            synchronized (this) {
-                if (!mOnChangeReceived) {
-                    try {
-                        wait(WAIT_FOR_CHANGE_MILLIS);
-                    } catch (InterruptedException e) {
-                    }
-                }
-            }
-            return mOnChangeReceived;
-        }
-    };
+    public PhotoProviderTest() {
+        super(PhotoProvider.class, PhotoProvider.AUTHORITY);
+    }
 
     @Override
-    protected void setUp() {
-        Context context = getInstrumentation().getTargetContext();
-        mDBHelper = new PhotoDatabase(context);
-        mResolver = context.getContentResolver();
+    protected void setUp() throws Exception {
+        super.setUp();
+        mResolver = getMockContentResolver();
+        PhotoProvider provider = (PhotoProvider) getProvider();
+        provider.setMockNotification(mNotifications);
+        mDBHelper = provider.getDatabaseHelper();
         SQLiteDatabase db = mDBHelper.getWritableDatabase();
         db.beginTransaction();
         try {
@@ -150,23 +85,18 @@
             mMetadataId = cursor.getLong(0);
             cursor.close();
             db.setTransactionSuccessful();
+            mNotifications.reset();
         } finally {
             db.endTransaction();
         }
     }
 
     @Override
-    protected void tearDown() {
-        SQLiteDatabase db = mDBHelper.getWritableDatabase();
-        db.beginTransaction();
-        try {
-            PhotoDatabaseUtils.deleteAllContent(db);
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-        }
+    protected void tearDown() throws Exception {
         mDBHelper.close();
         mDBHelper = null;
+        super.tearDown();
+        getMockContext().deleteDatabase(PhotoProvider.DB_NAME);
     }
 
     public void testDelete() {
@@ -204,85 +134,46 @@
         assertEquals(0, cursor.getCount());
         cursor.close();
     }
+
     // Delete the album and ensure that the photos referring to the album are
     // deleted.
     public void testDeleteAlbumCascade() {
-        WatchContentObserverThread observerThread = createObserverThread();
-        WatchContentObserver observer = observerThread.getObserver();
-        mResolver.registerContentObserver(Photos.CONTENT_URI, true, observer);
-        try {
-            Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId);
-            mResolver.delete(albumUri, null, null);
-            assertTrue(observer.waitForNotification());
-            assertEquals(observer.mUri, Photos.CONTENT_URI);
-            Cursor cursor = mResolver.query(Photos.CONTENT_URI,
-                    PhotoDatabaseUtils.PROJECTION_PHOTOS, null, null, null);
-            assertEquals(0, cursor.getCount());
-            cursor.close();
-        } finally {
-            mResolver.unregisterContentObserver(observer);
-            observerThread.stopLooper();
-        }
-    }
-
-    // Delete the album and ensure that the metadata referring to photos in that
-    // album are deleted.
-    public void testDeleteAlbumCascade2() {
-        WatchContentObserverThread observerThread = createObserverThread();
-        WatchContentObserver observer = observerThread.getObserver();
-        mResolver.registerContentObserver(Metadata.CONTENT_URI, true, observer);
-        try {
-            Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId);
-            mResolver.delete(albumUri, null, null);
-            assertTrue(observer.waitForNotification());
-            assertEquals(observer.mUri, Metadata.CONTENT_URI);
-            Cursor cursor = mResolver.query(Metadata.CONTENT_URI,
-                    PhotoDatabaseUtils.PROJECTION_METADATA, null, null, null);
-            assertEquals(0, cursor.getCount());
-            cursor.close();
-        } finally {
-            mResolver.unregisterContentObserver(observer);
-            observerThread.stopLooper();
-        }
+        Uri albumUri = ContentUris.withAppendedId(Albums.CONTENT_URI, mAlbumId);
+        mResolver.delete(albumUri, null, null);
+        assertTrue(mNotifications.isNotified(Photos.CONTENT_URI));
+        assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
+        assertTrue(mNotifications.isNotified(albumUri));
+        assertEquals(3, mNotifications.notificationCount());
+        Cursor cursor = mResolver.query(Photos.CONTENT_URI, PhotoDatabaseUtils.PROJECTION_PHOTOS,
+                null, null, null);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
     }
 
     // Delete all albums and ensure that photos in any album are deleted.
-    public void testDeleteAlbumCascade3() {
-        WatchContentObserverThread observerThread = createObserverThread();
-        WatchContentObserver observer = observerThread.getObserver();
-        mResolver.registerContentObserver(Photos.CONTENT_URI, true, observer);
-        try {
-            mResolver.delete(Albums.CONTENT_URI, null, null);
-            assertTrue(observer.waitForNotification());
-            assertEquals(observer.mUri, Photos.CONTENT_URI);
-            Cursor cursor = mResolver.query(Photos.CONTENT_URI,
-                    PhotoDatabaseUtils.PROJECTION_PHOTOS, null, null, null);
-            assertEquals(0, cursor.getCount());
-            cursor.close();
-        } finally {
-            mResolver.unregisterContentObserver(observer);
-            observerThread.stopLooper();
-        }
+    public void testDeleteAlbumCascade2() {
+        mResolver.delete(Albums.CONTENT_URI, null, null);
+        assertTrue(mNotifications.isNotified(Photos.CONTENT_URI));
+        assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
+        assertTrue(mNotifications.isNotified(Albums.CONTENT_URI));
+        assertEquals(3, mNotifications.notificationCount());
+        Cursor cursor = mResolver.query(Photos.CONTENT_URI, PhotoDatabaseUtils.PROJECTION_PHOTOS,
+                null, null, null);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
     }
 
     // Delete a photo and ensure that the metadata for that photo are deleted.
     public void testDeletePhotoCascade() {
-        WatchContentObserverThread observerThread = createObserverThread();
-        WatchContentObserver observer = observerThread.getObserver();
-        mResolver.registerContentObserver(Metadata.CONTENT_URI, true, observer);
-        try {
-            Uri albumUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId);
-            mResolver.delete(albumUri, null, null);
-            assertTrue(observer.waitForNotification());
-            assertEquals(observer.mUri, Metadata.CONTENT_URI);
-            Cursor cursor = mResolver.query(Metadata.CONTENT_URI,
-                    PhotoDatabaseUtils.PROJECTION_METADATA, null, null, null);
-            assertEquals(0, cursor.getCount());
-            cursor.close();
-        } finally {
-            mResolver.unregisterContentObserver(observer);
-            observerThread.stopLooper();
-        }
+        Uri photoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId);
+        mResolver.delete(photoUri, null, null);
+        assertTrue(mNotifications.isNotified(photoUri));
+        assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
+        assertEquals(2, mNotifications.notificationCount());
+        Cursor cursor = mResolver.query(Metadata.CONTENT_URI,
+                PhotoDatabaseUtils.PROJECTION_METADATA, null, null, null);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
     }
 
     public void testGetType() {
@@ -399,65 +290,19 @@
     }
 
     public void testUpdatePhotoNotification() {
-        WatchContentObserverThread observerThread = createObserverThread();
-        WatchContentObserver observer = observerThread.getObserver();
-        mResolver.registerContentObserver(Photos.CONTENT_URI, true, observer);
-        try {
-            Uri photoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId);
-            ContentValues values = new ContentValues();
-            values.put(Photos.MIME_TYPE, "not-a/mime-type");
-            mResolver.update(photoUri, values, null, null);
-            assertTrue(observer.waitForNotification());
-            assertEquals(observer.mUri, photoUri);
-        } finally {
-            mResolver.unregisterContentObserver(observer);
-            observerThread.stopLooper();
-        }
+        Uri photoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId);
+        ContentValues values = new ContentValues();
+        values.put(Photos.MIME_TYPE, "not-a/mime-type");
+        mResolver.update(photoUri, values, null, null);
+        assertTrue(mNotifications.isNotified(photoUri));
     }
 
     public void testUpdateMetadataNotification() {
-        WatchContentObserverThread observerThread = createObserverThread();
-        WatchContentObserver observer = observerThread.getObserver();
-        mResolver.registerContentObserver(Metadata.CONTENT_URI, true, observer);
-        try {
-            ContentValues values = new ContentValues();
-            values.put(Metadata.PHOTO_ID, mPhotoId);
-            values.put(Metadata.KEY, META_KEY);
-            values.put(Metadata.VALUE, "hello world");
-            mResolver.update(Metadata.CONTENT_URI, values, null, null);
-            assertTrue(observer.waitForNotification());
-            assertEquals(observer.mUri, Metadata.CONTENT_URI);
-        } finally {
-            mResolver.unregisterContentObserver(observer);
-            observerThread.stopLooper();
-        }
-    }
-
-    public void testDeletePhotoNotification() {
-        WatchContentObserverThread observerThread = createObserverThread();
-        WatchContentObserver observer = observerThread.getObserver();
-        mResolver.registerContentObserver(Photos.CONTENT_URI, true, observer);
-        try {
-            Uri photoUri = ContentUris.withAppendedId(Photos.CONTENT_URI, mPhotoId);
-            mResolver.delete(photoUri, null, null);
-            assertTrue(observer.waitForNotification());
-            assertEquals(observer.mUri, photoUri);
-        } finally {
-            mResolver.unregisterContentObserver(observer);
-            observerThread.stopLooper();
-        }
-    }
-
-    private WatchContentObserverThread createObserverThread() {
-        WatchContentObserverThread thread = new WatchContentObserverThread();
-        thread.start();
-        try {
-            thread.waitForObserver();
-            return thread;
-        } catch (InterruptedException e) {
-            thread.stopLooper();
-            fail("Interruption while waiting for observer being created.");
-            return null;
-        }
+        ContentValues values = new ContentValues();
+        values.put(Metadata.PHOTO_ID, mPhotoId);
+        values.put(Metadata.KEY, META_KEY);
+        values.put(Metadata.VALUE, "hello world");
+        mResolver.update(Metadata.CONTENT_URI, values, null, null);
+        assertTrue(mNotifications.isNotified(Metadata.CONTENT_URI));
     }
 }
diff --git a/tests/src/com/android/photos/data/TestHelper.java b/tests/src/com/android/photos/data/TestHelper.java
new file mode 100644
index 0000000..338e160
--- /dev/null
+++ b/tests/src/com/android/photos/data/TestHelper.java
@@ -0,0 +1,53 @@
+/*
+ * 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.util.Log;
+
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import java.lang.reflect.Method;
+
+public class TestHelper {
+    private static String TAG = TestHelper.class.getSimpleName();
+
+    public interface TestInitialization {
+        void initialize(TestCase testCase);
+    }
+
+    public static void addTests(Class<? extends TestCase> testClass, TestSuite suite,
+            TestInitialization initialization) {
+        for (Method method : testClass.getDeclaredMethods()) {
+            if (method.getName().startsWith("test") && method.getParameterTypes().length == 0) {
+                TestCase test;
+                try {
+                    test = testClass.newInstance();
+                    test.setName(method.getName());
+                    initialization.initialize(test);
+                    suite.addTest(test);
+                } catch (IllegalArgumentException e) {
+                    Log.e(TAG, "Failed to create test case", e);
+                } catch (InstantiationException e) {
+                    Log.e(TAG, "Failed to create test case", e);
+                } catch (IllegalAccessException e) {
+                    Log.e(TAG, "Failed to create test case", e);
+                }
+            }
+        }
+    }
+
+}