天天看點

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會自動完成這一過程