以下是測試對問題的描述:
有錄音檔案,解除安裝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會自動完成這一過程