以下是测试对问题的描述:
有录音文件,卸载SD卡后,手机内存中的录音文件不显示
【预置条件】保存有手机存储中的录音文件
【操作步骤】菜单--设置--存储--卸载SD卡--录音列表--观察
【实际结果】保存在手机内存的录音文件不显示
【预期结果】保存在手机内存中的录音文件应正常显示
【复现概率】必现
问题分析:
从问题的现象来看,是因为卸载了SD卡,导致原本能查找到的数据库内容变得不能被查到了,首先看录音文件列表的类RecordingFileList,其中cursor的创建如下:
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(MediaStore.Audio.Media.IS_RECORD);
stringBuilder.append(" =1");
String selection = stringBuilder.toString();
Cursor recordingFileCursor = getContentResolver().query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
new String[] {
MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM,
MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.DISPLAY_NAME, MediaStore.Audio.Media.DATE_ADDED,
MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media._ID
},selection, null, MediaStore.Audio.Media.DATE_ADDED + " DESC");//zhouwuping add "MediaStore.Audio.Media.DATE_ADDED + " DESC" to fix recordingfile sort by order for ACURAT-467.
从查询条件看,并没有与sd卡卸载相关的内容。于是怀疑是否有相关查询条件会在卸载sd卡后改变。
将复现问题的手机,导出数据库,可以查看到录音相关的数据库内容如下:
在external数据库中,两个录音文件对应的is_record字段是0
在external-5f7f0b31数据库中,两个录音文件对应的is_record字段是1
可注意到数据库中路径的不同,说明在卸载sd卡后,录音文件的地址是没有问题的,于是尝试修改stringBuilder,去除is_record的判断条件,问题消失。(但此时其他类型的音频文件也会显示出来了)
那么怀疑的对象就是为何is_record的值会变成0(并且此时,原本为0的is_music属性变成1了)
Is_record字段是mtk为了避免录音机中显示非录音文件而加入的字段,否则在recording目录下拷贝进去的音频文件也会显示在列表中。
录音完成后,将录音插到db中是没有问题的,代码如下:
cv.put(MediaStore.Audio.Media.IS_RECORD,"1");
那么问题肯定是发生在生成内置存储数据库的过程中了,猜测是由于某种疏漏,is_record字段没有被复制。而且is_record字段是项目中期mtk的代码升级添加的。
重新抓一个插着sd卡,设置默认存储是内部存储的录音log,可以看到在录音完成,已经插入到db中后,系统启动了mediascan,扫描对象即是刚生成的录音文件。
从log中可以找到,媒体扫描和database操作是在MediaScanner中完成的,对应的方法是doScanFile
可以看到在媒体扫描中有写入is_music的动作,相关代码如下:
判断是否是music:
boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
(!ringtones && !notifications && !alarms && !podcasts);
处理音频类型的文件:
if (isaudio || isvideo) {
processFile(path, mimeType, this);
}
因此这里首先要对music的赋值进行修改,需要判断文件的目录是否是在Recording下,增加一个属性:
boolean recordings = (lowpath.indexOf(RECORDING_DIR) > 0);//用于判断文件路径是否是在录音文件夹下
其中RECORDING_DIR是录音的文件名:
private static final String RECORDING_DIR = "/recording/";
这样就可以判断新增的文件是否是放在录音目录下了,这样is_music就不会变成1了,但这样会有别的问题,如果拷贝音频文件到Recording目录,这些文件的is_music也会被置为0。
如果去查询录音所在的数据库来判断,较为复杂,因此这儿先使用后缀名来进行判断,AL889的录音格式仅有3gpp和amr,做以下判断:
boolean recordings = (lowpath.indexOf(RECORDING_DIR) > 0 && ( mFileType == MediaFile.FILE_TYPE_3GPP3 || mFileType == MediaFile.FILE_TYPE_AMR));
数据库写入的最终执行,是在
private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
boolean alarms, boolean music, boolean podcasts)
中进行的,原生的endFile方法并没有recording的判断,因此需要改造下方法,加入recording参数,当然对于原来没有recording传参的调用,做以下处理:
private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
boolean alarms, boolean music, boolean podcasts)
throws RemoteException {
return endFile (entry, ringtones, notifications, alarms, music, podcasts, false);
}
这样在nomedia的条件下,也可以正确调用endFile方法了(仅在mediascanner内部被调用)
并且新的endFile方法如下:
private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
boolean alarms, boolean music, boolean podcasts, boolean recordings)
throws RemoteException {
……
values.put(Audio.Media.IS_PODCAST, podcasts);
values.put(Audio.Media.IS_RECORD, recordings);//添加这一行,即写入is_record字段
/// M: MAV type MPO file need parse some info from exif
} else if ((mFileType == MediaFile.FILE_TYPE_JPEG || mFileType == MediaFile.FILE_TYPE_MPO) && !mNoMedia) {
…
修改以上之后,再执行相同的步骤,录音文件能够显示正确,查看database文件,此时在external表中的is_record字段值为1,验证此方案是有效的。
PS:
录音文件的MediaScan是如何发起的?
录音完成后,通常的,在SoundRecorderService中的private Uri addToMediaDB(File file) 方法,负责将录音文件插入到数据库中。在方法的最后,执行了如下语句:
MediaScannerConnection.scanFile(getApplicationContext(), new String[] {file.getAbsolutePath()}, null, null);
因此在log中可以看到MediaScannerConnection首先启动了
01-03 05:28:27.956 V/MediaScannerConnection( 2661): Connected to Media Scanner
01-03 05:28:27.956 V/MediaScannerConnection( 2661): Scanning file /storage/sdcard1/Recording/record20140103052821.amr
(另一种发起单个文件扫描的方式是使用intent:ACTION_MEDIA_SCANNER_SCAN_FILE,当该intent被接收,就会发起扫描)
然后就是MediaScanner的部分了,调用scanFile方法后,MediaScannerConnection就会去binder对应的Service,就是MediaScannerService,具体可以看MediaScannerConnection中的onConnect方法
开始执行MediaScannerService中的查找文件方法:
首先在requestScanFile方法中,获取MediaScannerConnection处传入的参数,接着发了一个MSG_SCAN_SINGLE_FILE,于是通过handleScanSingleFile发起了单个文件的扫描。
在扫描时,会发现文件的卷被定义成了MediaProvider.EXTERNAL_VOLUME,也就是external卷,而目前兼容T卡的机子,通常会有external和external+ID两个卷。那么媒体扫描是如何区分的呢,通过MediaProvider的
private Uri attachVolume(String volume)
其中有判断条件如下:
String primaryPath = Environment.getExternalStorageDirectory().getPath();
String externalPath = StorageManagerEx.getExternalStoragePath();
boolean isExternalStorageRemovable = (primaryPath != null && primaryPath.equals(externalPath));
假如当前的外部存储是可卸载的(就是指T卡么),那么通过以下方法可以获取对应database的名字:
String dbName = "external-" + Integer.toHexString(volumeId) + ".db";
如果外部存储是不可卸载的话(没插T卡),那么数据库就会直接使用external.db,(里面还有个机制,如果没有external.db的话,会把external+id.db改名字,当然这种情况一般不发生)
经过上面的判断,databasehelper就创建好了,有t卡的话就是external+id.db,没有的话就是external.db。
而如上出现的卸载T卡事件,会被mediaProvider的mUnmountReceiver捕获,从而触发detachVolume(如果加载了T卡,则会执行attachVolume)
detachVolume这个方法,作用就是分离不生效的卷,可看到执行这个方法后,external+id.db这个database就被分离了,此时生效的是external.db。
01-02 04:02:11.448990 1270 3045 V MediaProvider: attachVolume>>> volume=external
01-02 04:02:11.449030 1270 3045 V MediaProvider: attachVolume<<< Already attached external.db
因此,在没有加载T卡的时候,使用的是external.db
至于external.db中的数据是如何写入的,在卸载/加载T卡的时候,mediascanner会自动完成这一过程