一、问题起源
最近有同事反馈试用的机器出现问题,图库的照片全部消失,新下载的第三方应用图片,也无法显示。针对该问题,当时以为是媒体库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, finalSQLiteDatabase db,finalString path) {final File nomedia = newFile(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(newRunnable() {
@Overridepublic voidrun() {
SystemClock.sleep(2000);if(nomedia.exists()) {
hidePath(helper, db, path);
}else{
Log.w(TAG,"does not exist: " + path, newException());
}
}}).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 voidhidePath(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= newFile(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 = newContentValues();
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
returnmClient.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,其定义如下:
@Overridepublic void scanFile(String path, long lastModified, longfileSize,boolean isDirectory, booleannoMedia) {//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系统上经常看到,也只能遇到一次规避一次。