Rework thumbnail cleanup

Bug: 63766886
Test: ran CTS tests
Change-Id: I1173f67d6fb41494d6d069cc7cfc0c2b0dcc1725
Merged-In: I1173f67d6fb41494d6d069cc7cfc0c2b0dcc1725
(cherry picked from commit f6b8f04d495b9dd5f4fc84db3313624ef5fc66d2)
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 1ff9cd3..65c9cd7 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -107,6 +107,7 @@
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -2099,7 +2100,7 @@
     }
 
     /**
-     * This method blocks until thumbnail is ready.
+     * This method requests a thumbnail and blocks until thumbnail is ready.
      *
      * @param thumbUri
      * @return
@@ -2312,7 +2313,7 @@
         int table = URI_MATCHER.match(uri);
         List<String> prependArgs = new ArrayList<String>();
 
-        // Log.v(TAG, "query: uri="+uri+", selection="+selection);
+        //Log.v(TAG, "query: uri="+uri+", selection="+selection);
         // handle MEDIA_SCANNER before calling getDatabaseForUri()
         if (table == MEDIA_SCANNER) {
             if (mMediaScannerVolume == null) {
@@ -3806,6 +3807,31 @@
         MediaScanner.clearMediaPathCache(true /* media */, false /* nomedia */);
         File nomedia = new File(path);
         String hiddenroot = nomedia.isDirectory() ? path : nomedia.getParent();
+
+        // query for images and videos that will be affected
+        Cursor c = db.query("files",
+                new String[] {"_id", "media_type"},
+                "_data >= ? AND _data < ? AND (media_type=1 OR media_type=3)"
+                + " AND mini_thumb_magic IS NOT NULL",
+                new String[] { hiddenroot  + "/", hiddenroot + "0"},
+                null /* groupBy */, null /* having */, null /* orderBy */);
+        if(c != null) {
+            if (c.getCount() != 0) {
+                Uri imagesUri = Uri.parse("content://media/external/images/media");
+                Uri videosUri = Uri.parse("content://media/external/videos/media");
+                while (c.moveToNext()) {
+                    // remove thumbnail for image/video
+                    long id = c.getLong(0);
+                    long mediaType = c.getLong(1);
+                    Log.i(TAG, "hiding image " + id + ", removing thumbnail");
+                    removeThumbnailFor(mediaType == FileColumns.MEDIA_TYPE_IMAGE ?
+                            imagesUri : videosUri, db, id);
+                }
+            }
+            IoUtils.closeQuietly(c);
+        }
+
+        // set the media type of the affected entries to 0
         ContentValues mediatype = new ContentValues();
         mediatype.put("media_type", 0);
         int numrows = db.update("files", mediatype,
@@ -4146,6 +4172,7 @@
                 editor.apply();
             }
             mMediaScannerVolume = null;
+            pruneThumbnails();
             return 1;
         }
 
@@ -4167,6 +4194,7 @@
             }
         } else {
             final String volumeName = getVolumeName(uri);
+            final boolean isExternal = "external".equals(volumeName);
 
             DatabaseHelper database = getDatabaseForUri(uri);
             if (database == null) {
@@ -4184,9 +4212,12 @@
                         database.mNumQueries++;
                         Cursor c = db.query(sGetTableAndWhereParam.table,
                                 sMediaTypeDataId,
-                                sGetTableAndWhereParam.where, whereArgs, null, null, null);
+                                sGetTableAndWhereParam.where, whereArgs,
+                                null /* groupBy */, null /* having */, null /* orderBy */);
                         String [] idvalue = new String[] { "" };
                         String [] playlistvalues = new String[] { "", "" };
+                        MiniThumbFile imageMicroThumbs = null;
+                        MiniThumbFile videoMicroThumbs = null;
                         try {
                             while (c.moveToNext()) {
                                 final int mediaType = c.getInt(0);
@@ -4201,7 +4232,9 @@
                                     idvalue[0] = String.valueOf(id);
                                     database.mNumQueries++;
                                     Cursor cc = db.query("thumbnails", sDataOnlyColumn,
-                                                "image_id=?", idvalue, null, null, null);
+                                                "image_id=?", idvalue,
+                                                null /* groupBy */, null /* having */,
+                                                null /* orderBy */);
                                     try {
                                         while (cc.moveToNext()) {
                                             deleteIfAllowed(uri, cc.getString(0));
@@ -4211,11 +4244,38 @@
                                     } finally {
                                         IoUtils.closeQuietly(cc);
                                     }
+                                    if (isExternal) {
+                                        if (imageMicroThumbs == null) {
+                                            imageMicroThumbs = MiniThumbFile.instance(
+                                                    Images.Media.EXTERNAL_CONTENT_URI);
+                                        }
+                                        imageMicroThumbs.eraseMiniThumb(id);
+                                    }
                                 } else if (mediaType == FileColumns.MEDIA_TYPE_VIDEO) {
                                     deleteIfAllowed(uri, data);
                                     MediaDocumentsProvider.onMediaStoreDelete(getContext(),
                                             volumeName, FileColumns.MEDIA_TYPE_VIDEO, id);
 
+                                    idvalue[0] = String.valueOf(id);
+                                    database.mNumQueries++;
+                                    Cursor cc = db.query("videothumbnails", sDataOnlyColumn,
+                                                "video_id=?", idvalue, null, null, null);
+                                    try {
+                                        while (cc.moveToNext()) {
+                                            deleteIfAllowed(uri, cc.getString(0));
+                                        }
+                                        database.mNumDeletes++;
+                                        db.delete("videothumbnails", "video_id=?", idvalue);
+                                    } finally {
+                                        IoUtils.closeQuietly(cc);
+                                    }
+                                    if (isExternal) {
+                                        if (videoMicroThumbs == null) {
+                                            videoMicroThumbs = MiniThumbFile.instance(
+                                                    Video.Media.EXTERNAL_CONTENT_URI);
+                                        }
+                                        videoMicroThumbs.eraseMiniThumb(id);
+                                    }
                                 } else if (mediaType == FileColumns.MEDIA_TYPE_AUDIO) {
                                     if (!database.mInternal) {
                                         MediaDocumentsProvider.onMediaStoreDelete(getContext(),
@@ -4251,6 +4311,12 @@
                             }
                         } finally {
                             IoUtils.closeQuietly(c);
+                            if (imageMicroThumbs != null) {
+                                imageMicroThumbs.deactivate();
+                            }
+                            if (videoMicroThumbs != null) {
+                                videoMicroThumbs.deactivate();
+                            }
                         }
                     }
                     // Do not allow deletion if the file/object is referenced as parent
@@ -4333,12 +4399,92 @@
         throw new UnsupportedOperationException("Unsupported call: " + method);
     }
 
+
+    /*
+     * Clean up all thumbnail files for which the source image or video no longer exists.
+     * This is called at the end of a media scan.
+     */
+    private void pruneThumbnails() {
+        Log.v(TAG, "pruneThumbnails ");
+
+        final Uri thumbsUri = Images.Thumbnails.getContentUri("external");
+
+        // Remove orphan entries in the thumbnails tables
+        DatabaseHelper helper = getDatabaseForUri(thumbsUri);
+        SQLiteDatabase db = helper.getWritableDatabase();
+        db.execSQL("delete from thumbnails where image_id not in (select _id from images)");
+        db.execSQL("delete from videothumbnails where video_id not in (select _id from video)");
+
+        // Remove cached thumbnails that are no longer referenced by the thumbnails tables
+        HashSet<String> existingFiles = new HashSet<String>();
+        try {
+            String directory = "/sdcard/DCIM/.thumbnails";
+            File dirFile = new File(directory).getCanonicalFile();
+            String[] files = dirFile.list();
+            if (files == null)
+                files = new String[0];
+
+            String dirPath = dirFile.getPath();
+            for (int i = 0; i < files.length; i++) {
+                if (files[i].endsWith(".jpg")) {
+                    String fullPathString = dirPath + "/" + files[i];
+                    existingFiles.add(fullPathString);
+                }
+            }
+        } catch (IOException e) {
+            return;
+        }
+
+        for (String table : new String[] {"thumbnails", "videothumbnails"}) {
+            Cursor c = db.query(table, new String [] { "_data" },
+                    null, null, null, null, null); // where clause/args, groupby, having, orderby
+            if (c != null && c.moveToFirst()) {
+                do {
+                    String fullPathString = c.getString(0);
+                    existingFiles.remove(fullPathString);
+                } while (c.moveToNext());
+            }
+            IoUtils.closeQuietly(c);
+        }
+
+        for (String fileToDelete : existingFiles) {
+            if (LOCAL_LOGV)
+                Log.v(TAG, "fileToDelete is " + fileToDelete);
+            try {
+                (new File(fileToDelete)).delete();
+            } catch (SecurityException ex) {
+            }
+        }
+
+        Log.v(TAG, "/pruneDeadThumbnailFiles... ");
+    }
+
+    private void removeThumbnailFor(Uri uri, SQLiteDatabase db, long id) {
+        Cursor c = db.rawQuery("select _data from thumbnails where image_id=" + id
+                + " union all select _data from videothumbnails where video_id=" + id,
+                null /* selectionArgs */);
+        if (c != null) {
+            while (c.moveToNext()) {
+                String path = c.getString(0);
+                deleteIfAllowed(uri, path);
+            }
+            IoUtils.closeQuietly(c);
+            db.execSQL("delete from thumbnails where image_id=" + id);
+            db.execSQL("delete from videothumbnails where video_id=" + id);
+        }
+        MiniThumbFile microThumbs = MiniThumbFile.instance(uri);
+        microThumbs.eraseMiniThumb(id);
+        microThumbs.deactivate();
+    }
+
     @Override
     public int update(Uri uri, ContentValues initialValues, String userWhere,
             String[] whereArgs) {
         uri = safeUncanonicalize(uri);
         int count;
-        // Log.v(TAG, "update for uri="+uri+", initValues="+initialValues);
+        //Log.v(TAG, "update for uri=" + uri + ", initValues=" + initialValues +
+        //        ", where=" + userWhere + ", args=" + Arrays.toString(whereArgs) + " caller:" +
+        //        Binder.getCallingPid());
         int match = URI_MATCHER.match(uri);
         DatabaseHelper helper = getDatabaseForUri(uri);
         if (helper == null) {
@@ -4358,6 +4504,33 @@
         synchronized (sGetTableAndWhereParam) {
             getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam);
 
+            // if the media type is being changed, check if it's being changed from image or video
+            // to something else
+            if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) {
+                long newMediaType = initialValues.getAsLong(FileColumns.MEDIA_TYPE);
+                helper.mNumQueries++;
+                Cursor cursor = db.query(sGetTableAndWhereParam.table, sMediaTableColumns,
+                    sGetTableAndWhereParam.where, whereArgs, null, null, null);
+                try {
+                    while (cursor != null && cursor.moveToNext()) {
+                        long curMediaType = cursor.getLong(1);
+                        if (curMediaType == FileColumns.MEDIA_TYPE_IMAGE &&
+                                newMediaType != FileColumns.MEDIA_TYPE_IMAGE) {
+                            Log.i(TAG, "need to remove image thumbnail for id " + cursor.getString(0));
+                            removeThumbnailFor(Images.Media.EXTERNAL_CONTENT_URI,
+                                    db, cursor.getLong(0));
+                        } else if (curMediaType == FileColumns.MEDIA_TYPE_VIDEO &&
+                                newMediaType != FileColumns.MEDIA_TYPE_VIDEO) {
+                            Log.i(TAG, "need to remove video thumbnail for id " + cursor.getString(0));
+                            removeThumbnailFor(Video.Media.EXTERNAL_CONTENT_URI,
+                                    db, cursor.getLong(0));
+                        }
+                    }
+                } finally {
+                    IoUtils.closeQuietly(cursor);
+                }
+            }
+
             // special case renaming directories via MTP.
             // in this case we must update all paths in the database with
             // the directory name as a prefix