天天看点

Android 有录音文件,卸载SD卡后,手机内存中的录音文件不显示问题分析与修改

以下是测试对问题的描述:

有录音文件,卸载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会自动完成这一过程