Android nomedia问题分析

一、问题起源

  最近有同事反馈试用的机器出现问题,图库的照片全部消失,新下载的第三方应用图片,也无法显示。针对该问题,当时以为是媒体库scan过程和数据库存在异常,查了半天无任何结论。内部讨论后,初步怀疑是nomedia导致,查看外置存储根目录的隐藏文件,果然有.nomdia生成,但这个是谁生成的呢?无从知晓,随后让同事提供试用过程,一步步盘查,结果定位到国内某度应用导致。对比国内其他机器,无此问题,应该是规避了。那么如何规避该问题,删除此文件或者排除此路径的隐藏机制?

 

二、nomedia实现方式

  既然规避,自然需要弄清楚系统如何实现nomedia隐藏的机制。那么nomedia到底如何定义的呢?

frameworks/base/core/java/android/provider/MediaStore.java

/**
 * Name of the file signaling the media scanner to ignore media in the containing directory
 * and its subdirectories. Developers should use this to avoid application graphics showing
 * up in the Gallery and likewise prevent application sounds and music from showing up in
 * the Music app.
 */
public static final String MEDIA_IGNORE_FILENAME = ".nomedia";
  如上定义,顾名思义,是隐藏此文件当前目录以及子目录的媒体文件。那么系统是如何利用.nomedia实现该机制的呢?
根据代码搜索到的路径分析,目前有两个地方进行了隐藏处理,MediaProvider和MediaScanner,下面先看MediaProvider:
1、MediaProvider
packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
    /*
     * Sets the media type of all files below the newly added .nomedia file or
     * hidden folder to 0, so the entries no longer appear in e.g. the audio and
     * images views.
     *
     * @param path The path to the new .nomedia file or hidden directory
     */
    private void processNewNoMediaPath(final DatabaseHelper helper, final SQLiteDatabase db,
            final String path) {
        final File nomedia = new File(path);
        if (nomedia.exists()) {
            hidePath(helper, db, path);
        } else {
            // File doesn't exist. Try again in a little while.
            // XXX there's probably a better way of doing this
            new Thread(new Runnable() {
                @Override
                public void run() {
                    SystemClock.sleep(2000);
                    if (nomedia.exists()) {
                        hidePath(helper, db, path);
                    } else {
                        Log.w(TAG, "does not exist: " + path, new Exception());
                    }
                }}).start();
        }
    }

可以看到processNewNoMediaPath方法对.nomedia进行隐藏处理,判断的代码如下:

媒体库update时:

            } else if (newPath.toLowerCase(Locale.US).endsWith("/.nomedia")) {
                processNewNoMediaPath(helper, db, newPath);
            }

媒体库insertInternal:

        if (path != null && path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
            // need to set the media_type of all the files below this folder to 0
            processNewNoMediaPath(helper, db, path);
        }
        return newUri;

下面看下processNewNoMediaPath方法如何实现隐藏的:

processNewNoMediaPath方法中调用了hidePath进行隐藏实现,而hidePath方法的关键是将媒体库中的media_type更新为0:

    private void hidePath(DatabaseHelper helper, SQLiteDatabase db, String path) {
        // a new nomedia path was added, so clear the media paths
        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,
                "_data >= ? AND _data < ?",
                new String[] { hiddenroot  + "/", hiddenroot + "0"});
        helper.mNumUpdates += numrows;
        ContentResolver res = getContext().getContentResolver();
        res.notifyChange(Uri.parse("content://media/"), null);
    }

以上实现了媒体库的文件隐藏。下面来看MediaScanner的过程:

2、MediaScanner

frameworks/base/media/java/android/media/MediaScanner.java

isNoMediaPath中:

                  // check to see if any parent directories have a ".nomedia" file
1500                  // start from 1 so we don't bother checking in the root directory
1501                  int offset = 1;
1502                  while (offset >= 0) {
1503                      int slashIndex = path.indexOf('/', offset);
1504                      if (slashIndex > offset) {
1505                          slashIndex++; // move past slash
1506                          File file = new File(path.substring(0, slashIndex) + ".nomedia");
1507                          if (file.exists()) {
1508                              // we have a .nomedia in one of the parent directories
1509                              mNoMediaPaths.put(parent, "");
1510                              return true;
1511                          }
1512                      }

这里可以看到在 isNoMediaPath方法中,每次扫描到含有.nomedia的路径,都会被添加到mNoMediaPaths的map中。下面看下此方法的作用:

endfile中:

                int mediaType = 0;
                if (!MediaScanner.isNoMediaPath(entry.mPath)) {
                    int fileType = MediaFile.getFileTypeForMimeType(mMimeType);
                    if (MediaFile.isAudioFileType(fileType)) {
                        mediaType = FileColumns.MEDIA_TYPE_AUDIO;
                    } else if (MediaFile.isVideoFileType(fileType)) {
                        mediaType = FileColumns.MEDIA_TYPE_VIDEO;
                    } else if (MediaFile.isImageFileType(fileType)) {
                        mediaType = FileColumns.MEDIA_TYPE_IMAGE;
                    } else if (MediaFile.isPlayListFileType(fileType)) {
                        mediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
                    }
                    values.put(FileColumns.MEDIA_TYPE, mediaType);
                }
                mMediaProvider.update(result, values, null, null);

 scanSignleFile中:

// 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, MediaScanner.isNoMediaPath(path));

下面分析doScanFile:

此方法除了被scanSingleFile调用完,还被scanFile调用,说明是MediaScanner隐藏媒体文件机制的关键,下面看其实现:

                FileEntry entry = beginFile(path, mimeType, lastModified,
                        fileSize, isDirectory, noMedia);

其又调用了beginFile,又做了下面判断:

                // rescan for metadata if file was modified since last scan
                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
                    if (noMedia) {
                        result = endFile(entry, false, false, false, false, false);
                    } else {
                        String lowpath = path.toLowerCase(Locale.ROOT);
                        boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);

beginFile:

            if (!isDirectory) {
                if (!noMedia && isNoMediaFile(path)) {
                    noMedia = true;
                }
                mNoMedia = noMedia;

这里mNoMedia就是关键了,调用如下:

endFile中:

            if (!mNoMedia) {
                if (MediaFile.isVideoFileType(mFileType)) {
                    tableUri = mVideoUri;
                } else if (MediaFile.isImageFileType(mFileType)) {
                    tableUri = mImagesUri;
                } else if (MediaFile.isAudioFileType(mFileType)) {
                    tableUri = mAudioUri;
                }
            }

toValue中:

            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);

本次我们追踪的是.nomedia文件隐藏机制,可以看到与传入的noMedia的值有关,noMedia和mNoMedia决定了扫描到的媒体数据是否保存,而mNoMedia在本次分析中又取决于传入的noMedia,那么noMedia的值是如何来的呢?前面我们已经知道部分是 scanSignleFile中的isNoMediaPath调用值,另外的就是scanFile,其定义如下:

        @Override
        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, noMedia);
        }

这个值又是native传过来的,继续追踪native的流程,最终定位到下面流程:

frameworks/av/media/libmedia/MediaScanner.cpp

    // 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) {
            ALOGV("found .nomedia, setting noMedia flag");
            noMedia = true;
        }

        // restore path
        fileSpot[0] = 0;
    }

理清了上面的处理流程,接下来问题的解决就清晰了。

 

三、总结

  本次处理的问题,应该是三方应用设计不规范导致,系统提供的nomedia机制本来是方便应用隐藏缓存文件,结果有些app设计者不清楚其实现机制,随意创建该文件,导致出现本问题。从用户角度考虑,该问题其实是系统的设计缺陷,不能因为ap调用不规范就引起其他应用出现问题,此类问题在Android系统上经常看到,也只能遇到一次规避一次。

 

 

 

 



 

posted @ 2018-06-29 14:20  elenin  阅读(1093)  评论(0编辑  收藏  举报