Merge "MediaScanner: reimplement the ".nomedia" feature for hiding files from the media provider"
diff --git a/include/media/mediascanner.h b/include/media/mediascanner.h
index df5be32..765c039 100644
--- a/include/media/mediascanner.h
+++ b/include/media/mediascanner.h
@@ -55,7 +55,7 @@
 
     status_t doProcessDirectory(
             char *path, int pathRemaining, MediaScannerClient &client,
-            ExceptionCheck exceptionCheck, void *exceptionEnv);
+            bool noMedia, ExceptionCheck exceptionCheck, void *exceptionEnv);
 
     MediaScanner(const MediaScanner &);
     MediaScanner &operator=(const MediaScanner &);
@@ -72,10 +72,9 @@
     void endFile();
 
     virtual bool scanFile(const char* path, long long lastModified,
-            long long fileSize, bool isDirectory) = 0;
+            long long fileSize, bool isDirectory, bool noMedia) = 0;
     virtual bool handleStringTag(const char* name, const char* value) = 0;
     virtual bool setMimeType(const char* mimeType) = 0;
-    virtual bool addNoMediaFolder(const char* path) = 0;
 
 protected:
     void convertValues(uint32_t encoding);
diff --git a/media/java/android/media/MediaScanner.java b/media/java/android/media/MediaScanner.java
index a7ac9ed..80cc94e 100644
--- a/media/java/android/media/MediaScanner.java
+++ b/media/java/android/media/MediaScanner.java
@@ -422,9 +422,10 @@
         private long mFileSize;
         private String mWriter;
         private int mCompilation;
+        private boolean mNoMedia;   // flag to suppress file from appearing in media tables
 
         public FileCacheEntry beginFile(String path, String mimeType, long lastModified,
-                long fileSize, boolean isDirectory) {
+                long fileSize, boolean isDirectory, boolean noMedia) {
             mMimeType = mimeType;
             mFileType = 0;
             mFileSize = fileSize;
@@ -435,28 +436,31 @@
                 // to avoid memory allocation
                 int lastSlash = path.lastIndexOf('/');
                 if (lastSlash >= 0 && lastSlash + 2 < path.length()) {
-                    // ignore those ._* files created by MacOS
-                    if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
-                        return null;
-                    }
-
-                    // ignore album art files created by Windows Media Player:
-                    // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
-                    // and AlbumArt_{...}_Small.jpg
-                    if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
-                        if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
-                                path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
-                            return null;
+                    if (!noMedia) {
+                        // ignore those ._* files created by MacOS
+                        if (path.regionMatches(lastSlash + 1, "._", 0, 2)) {
+                            noMedia = true;
                         }
-                        int length = path.length() - lastSlash - 1;
-                        if ((length == 17 && path.regionMatches(
-                                true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
-                                (length == 10
-                                 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
-                            return null;
+
+                        // ignore album art files created by Windows Media Player:
+                        // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg
+                        // and AlbumArt_{...}_Small.jpg
+                        if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) {
+                            if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) ||
+                                    path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) {
+                                noMedia = true;
+                            }
+                            int length = path.length() - lastSlash - 1;
+                            if ((length == 17 && path.regionMatches(
+                                    true, lastSlash + 1, "AlbumArtSmall", 0, 13)) ||
+                                    (length == 10
+                                     && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) {
+                                noMedia = true;
+                            }
                         }
                     }
                 }
+                mNoMedia = noMedia;
 
                 // try mimeType first, if it is specified
                 if (mimeType != null) {
@@ -523,36 +527,41 @@
             return entry;
         }
 
-        public void scanFile(String path, long lastModified, long fileSize, boolean isDirectory) {
+        public void scanFile(String path, long lastModified, long fileSize,
+                boolean isDirectory, boolean noMedia) {
             // This is the callback funtion from native codes.
             // Log.v(TAG, "scanFile: "+path);
-            doScanFile(path, null, lastModified, fileSize, isDirectory, false);
+            doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
         }
 
         public Uri doScanFile(String path, String mimeType, long lastModified,
-                long fileSize, boolean isDirectory, boolean scanAlways) {
+                long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
             Uri result = null;
 //            long t1 = System.currentTimeMillis();
             try {
                 FileCacheEntry entry = beginFile(path, mimeType, lastModified,
-                        fileSize, isDirectory);
+                        fileSize, isDirectory, noMedia);
                 // rescan for metadata if file was modified since last scan
                 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
-                    String lowpath = path.toLowerCase();
-                    boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
-                    boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
-                    boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
-                    boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
-                    boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
-                        (!ringtones && !notifications && !alarms && !podcasts);
+                    if (noMedia) {
+                        result = endFile(entry, false, false, false, false, false);
+                    } else {
+                        String lowpath = path.toLowerCase();
+                        boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
+                        boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
+                        boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
+                        boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
+                        boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
+                            (!ringtones && !notifications && !alarms && !podcasts);
 
-                    // we only extract metadata for audio and video files
-                    if (MediaFile.isAudioFileType(mFileType)
-                            || MediaFile.isVideoFileType(mFileType)) {
-                        processFile(path, mimeType, this);
+                        // we only extract metadata for audio and video files
+                        if (MediaFile.isAudioFileType(mFileType)
+                                || MediaFile.isVideoFileType(mFileType)) {
+                            processFile(path, mimeType, this);
+                        }
+
+                        result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
                     }
-
-                    result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
                 }
             } catch (RemoteException e) {
                 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
@@ -689,27 +698,31 @@
             map.put(MediaStore.MediaColumns.SIZE, mFileSize);
             map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType);
 
-            if (MediaFile.isVideoFileType(mFileType)) {
-                map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaStore.UNKNOWN_STRING));
-                map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaStore.UNKNOWN_STRING));
-                map.put(Video.Media.DURATION, mDuration);
-                // FIXME - add RESOLUTION
-            } else if (MediaFile.isImageFileType(mFileType)) {
-                // FIXME - add DESCRIPTION
-            } else if (MediaFile.isAudioFileType(mFileType)) {
-                map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
-                        mArtist : MediaStore.UNKNOWN_STRING);
-                map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
-                        mAlbumArtist.length() > 0) ? mAlbumArtist : null);
-                map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
-                        mAlbum : MediaStore.UNKNOWN_STRING);
-                map.put(Audio.Media.COMPOSER, mComposer);
-                if (mYear != 0) {
-                    map.put(Audio.Media.YEAR, mYear);
+            if (!mNoMedia) {
+                if (MediaFile.isVideoFileType(mFileType)) {
+                    map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0
+                            ? mArtist : MediaStore.UNKNOWN_STRING));
+                    map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0
+                            ? mAlbum : MediaStore.UNKNOWN_STRING));
+                    map.put(Video.Media.DURATION, mDuration);
+                    // FIXME - add RESOLUTION
+                } else if (MediaFile.isImageFileType(mFileType)) {
+                    // FIXME - add DESCRIPTION
+                } else if (MediaFile.isAudioFileType(mFileType)) {
+                    map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ?
+                            mArtist : MediaStore.UNKNOWN_STRING);
+                    map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null &&
+                            mAlbumArtist.length() > 0) ? mAlbumArtist : null);
+                    map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ?
+                            mAlbum : MediaStore.UNKNOWN_STRING);
+                    map.put(Audio.Media.COMPOSER, mComposer);
+                    if (mYear != 0) {
+                        map.put(Audio.Media.YEAR, mYear);
+                    }
+                    map.put(Audio.Media.TRACK, mTrack);
+                    map.put(Audio.Media.DURATION, mDuration);
+                    map.put(Audio.Media.COMPILATION, mCompilation);
                 }
-                map.put(Audio.Media.TRACK, mTrack);
-                map.put(Audio.Media.DURATION, mDuration);
-                map.put(Audio.Media.COMPILATION, mCompilation);
             }
             return map;
         }
@@ -719,7 +732,7 @@
                 throws RemoteException {
             // update database
 
-             // use album artist if artist is missing
+            // use album artist if artist is missing
             if (mArtist == null || mArtist.length() == 0) {
                 mArtist = mAlbumArtist;
             }
@@ -761,7 +774,7 @@
                 values.put(Audio.Media.IS_ALARM, alarms);
                 values.put(Audio.Media.IS_MUSIC, music);
                 values.put(Audio.Media.IS_PODCAST, podcasts);
-            } else if (mFileType == MediaFile.FILE_TYPE_JPEG) {
+            } else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) {
                 ExifInterface exif = null;
                 try {
                     exif = new ExifInterface(entry.mPath);
@@ -814,12 +827,14 @@
             }
 
             Uri tableUri = mFilesUri;
-            if (MediaFile.isVideoFileType(mFileType)) {
-                tableUri = mVideoUri;
-            } else if (MediaFile.isImageFileType(mFileType)) {
-                tableUri = mImagesUri;
-            } else if (MediaFile.isAudioFileType(mFileType)) {
-                tableUri = mAudioUri;
+            if (!mNoMedia) {
+                if (MediaFile.isVideoFileType(mFileType)) {
+                    tableUri = mVideoUri;
+                } else if (MediaFile.isImageFileType(mFileType)) {
+                    tableUri = mImagesUri;
+                } else if (MediaFile.isAudioFileType(mFileType)) {
+                    tableUri = mAudioUri;
+                }
             }
             Uri result = null;
             if (rowId == 0) {
@@ -930,25 +945,6 @@
             }
         }
 
-        public void addNoMediaFolder(String path) {
-            ContentValues values = new ContentValues();
-            values.put(MediaStore.Images.ImageColumns.DATA, "");
-            String [] pathSpec = new String[] {path + '%'};
-            try {
-                // These tables have DELETE_FILE triggers that delete the file from the
-                // sd card when deleting the database entry. We don't want to do this in
-                // this case, since it would cause those files to be removed if a .nomedia
-                // file was added after the fact, when in that case we only want the database
-                // entries to be removed.
-                mMediaProvider.update(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values,
-                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
-                mMediaProvider.update(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values,
-                        MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec);
-            } catch (RemoteException e) {
-                throw new RuntimeException();
-            }
-        }
-
         private int getFileTypeFromDrm(String path) {
             if (!isDrmEnabled()) {
                 return 0;
@@ -1228,13 +1224,37 @@
 
             // always scan the file, so we can return the content://media Uri for existing files
             return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
-                    false, true);
+                    false, true, false);
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
             return null;
         }
     }
 
+    public static boolean isNoMediaPath(String path) {
+        if (path == null) return false;
+
+        // return true if file or any parent directory has name starting with a dot
+        if (path.indexOf("/.") >= 0) return true;
+
+        // now check to see if any parent directories have a ".nomedia" file
+        // start from 1 so we don't bother checking in the root directory
+        int offset = 1;
+        while (offset >= 0) {
+            int slashIndex = path.indexOf('/', offset);
+            if (slashIndex > offset) {
+                slashIndex++; // move past slash
+                File file = new File(path.substring(0, slashIndex) + ".nomedia");
+                if (file.exists()) {
+                    // we have a .nomedia in one of the parent directories
+                    return true;
+                }
+            }
+            offset = slashIndex;
+        }
+        return false;
+    }
+
     public void scanMtpFile(String path, String volumeName, int objectHandle, int format) {
         initialize(volumeName);
         MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
@@ -1279,7 +1299,7 @@
 
                 // always scan the file, so we can return the content://media Uri for existing files
                 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(),
-                    (format == MtpConstants.FORMAT_ASSOCIATION), true);
+                    (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path));
             }
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
diff --git a/media/java/android/media/MediaScannerClient.java b/media/java/android/media/MediaScannerClient.java
index ac326ef..b326671 100644
--- a/media/java/android/media/MediaScannerClient.java
+++ b/media/java/android/media/MediaScannerClient.java
@@ -21,9 +21,8 @@
  */
 public interface MediaScannerClient
 {    
-    public void scanFile(String path, long lastModified, long fileSize, boolean isDirectory);
-
-    public void addNoMediaFolder(String path);
+    public void scanFile(String path, long lastModified, long fileSize,
+            boolean isDirectory, boolean noMedia);
 
     /**
      * Called by native code to return metadata extracted from media files.
diff --git a/media/jni/android_media_MediaScanner.cpp b/media/jni/android_media_MediaScanner.cpp
index a3dd136..9151799 100644
--- a/media/jni/android_media_MediaScanner.cpp
+++ b/media/jni/android_media_MediaScanner.cpp
@@ -67,7 +67,7 @@
             mScanFileMethodID = env->GetMethodID(
                                     mediaScannerClientInterface,
                                     "scanFile",
-                                    "(Ljava/lang/String;JJZ)V");
+                                    "(Ljava/lang/String;JJZZ)V");
 
             mHandleStringTagMethodID = env->GetMethodID(
                                     mediaScannerClientInterface,
@@ -78,11 +78,6 @@
                                     mediaScannerClientInterface,
                                     "setMimeType",
                                     "(Ljava/lang/String;)V");
-
-            mAddNoMediaFolderMethodID = env->GetMethodID(
-                                    mediaScannerClientInterface,
-                                    "addNoMediaFolder",
-                                    "(Ljava/lang/String;)V");
         }
     }
 
@@ -95,7 +90,7 @@
     // Returns true if it succeeded, false if an exception occured
     // in the Java code
     virtual bool scanFile(const char* path, long long lastModified,
-            long long fileSize, bool isDirectory)
+            long long fileSize, bool isDirectory, bool noMedia)
     {
         LOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
             path, lastModified, fileSize, isDirectory);
@@ -106,7 +101,7 @@
         }
 
         mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
-                fileSize, isDirectory);
+                fileSize, isDirectory, noMedia);
 
         mEnv->DeleteLocalRef(pathStr);
         return (!mEnv->ExceptionCheck());
@@ -149,30 +144,12 @@
         return (!mEnv->ExceptionCheck());
     }
 
-    // Returns true if it succeeded, false if an exception occured
-    // in the Java code
-    virtual bool addNoMediaFolder(const char* path)
-    {
-        LOGV("addNoMediaFolder: path(%s)", path);
-        jstring pathStr;
-        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
-            return false;
-        }
-
-        mEnv->CallVoidMethod(mClient, mAddNoMediaFolderMethodID, pathStr);
-
-        mEnv->DeleteLocalRef(pathStr);
-        return (!mEnv->ExceptionCheck());
-    }
-
-
 private:
     JNIEnv *mEnv;
     jobject mClient;
     jmethodID mScanFileMethodID;
     jmethodID mHandleStringTagMethodID;
     jmethodID mSetMimeTypeMethodID;
-    jmethodID mAddNoMediaFolderMethodID;
 };
 
 
diff --git a/media/libmedia/MediaScanner.cpp b/media/libmedia/MediaScanner.cpp
index 5ec573e..4e22175 100644
--- a/media/libmedia/MediaScanner.cpp
+++ b/media/libmedia/MediaScanner.cpp
@@ -70,8 +70,7 @@
     client.setLocale(locale());
 
     status_t result =
-        doProcessDirectory(
-                pathBuffer, pathRemaining, client, exceptionCheck, exceptionEnv);
+        doProcessDirectory(pathBuffer, pathRemaining, client, false, exceptionCheck, exceptionEnv);
 
     free(pathBuffer);
 
@@ -80,20 +79,18 @@
 
 status_t MediaScanner::doProcessDirectory(
         char *path, int pathRemaining, MediaScannerClient &client,
-        ExceptionCheck exceptionCheck, void *exceptionEnv) {
+        bool noMedia, ExceptionCheck exceptionCheck, void *exceptionEnv) {
     // place to copy file or directory name
     char* fileSpot = path + strlen(path);
     struct dirent* entry;
     struct stat statbuf;
 
-    // ignore directories that contain a  ".nomedia" file
+    // Treat all files as non-media in directories that contain a  ".nomedia" file
     if (pathRemaining >= 8 /* strlen(".nomedia") */ ) {
         strcpy(fileSpot, ".nomedia");
         if (access(path, F_OK) == 0) {
-            LOGD("found .nomedia, skipping directory\n");
-            fileSpot[0] = 0;
-            client.addNoMediaFolder(path);
-            return OK;
+            LOGD("found .nomedia, setting noMedia flag\n");
+            noMedia = true;
         }
 
         // restore path
@@ -138,19 +135,20 @@
         }
         if (type == DT_REG || type == DT_DIR) {
             if (type == DT_DIR) {
-                // ignore directories with a name that starts with '.'
+                // set noMedia flag on directories with a name that starts with '.'
                 // for example, the Mac ".Trashes" directory
-                if (name[0] == '.') continue;
+                if (name[0] == '.')
+                    noMedia = true;
 
                 // report the directory to the client
                 if (stat(path, &statbuf) == 0) {
-                    client.scanFile(path, statbuf.st_mtime, 0, true);
+                    client.scanFile(path, statbuf.st_mtime, 0, true, noMedia);
                 }
 
                 // and now process its contents
                 strcat(fileSpot, "/");
                 int err = doProcessDirectory(path, pathRemaining - nameLength - 1, client,
-                        exceptionCheck, exceptionEnv);
+                        noMedia, exceptionCheck, exceptionEnv);
                 if (err) {
                     // pass exceptions up - ignore other errors
                     if (exceptionCheck && exceptionCheck(exceptionEnv)) goto failure;
@@ -159,7 +157,7 @@
                 }
             } else {
                 stat(path, &statbuf);
-                client.scanFile(path, statbuf.st_mtime, statbuf.st_size, false);
+                client.scanFile(path, statbuf.st_mtime, statbuf.st_size, false, noMedia);
                 if (exceptionCheck && exceptionCheck(exceptionEnv)) goto failure;
             }
         }