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系统上经常看到,也只能遇到一次规避一次。