Android Q 與 SdCard 的恩恩怨怨

 

     Android Q的第6個Beta版本已經發出,距離正式版本推出非常臨近了. 筆者"有幸"提前嚐到Android Q的"酸爽",特此留下此篇以給後面的攻城獅拋磚引玉.

    Android Q的更新比較多,但是與我們應用層App開發者影響最大還是 Q與內存卡的恩恩怨怨; 爲啥Android Q 突然要搞出這種幺蛾子了? Google 爸爸的衆多理由有2個最突出對用戶最友好的是:1.減少應用權限的申請2.這樣可以讓SdCard裏面的存儲空間更加整潔. 以前各種App動不動就各種在SdCard 裏面新建各種文件,搞的內存卡凌亂無比.  Environment.getExternalStorageDirectory() 這個用着很爽吧,對不起以後Android Q上不能用了. What ? Google你這是要鬧哪樣 ?  Google 爸爸給出了自己相應的方案-----分區存儲 

   簡單來說以筆者的理解就是(如有錯誤之處還望各位看官不吝斧正) : 一個類似IOS的半吊子的沙盒. 在android Q上面每個app在 內存卡上面有個屬於自己的沙盒(獨立空間 -- /storage/emulated/0/Android/data/包名 ) 別的App是無法直接訪問和獲取該沙盒信息的.自己的App在自己的沙盒裏面 讀取/書寫 文件IO操作是不要任何的權限的. app內存卡沙盒的根路徑通過該:Context.getExternalFilesDir() 可獲.同時該沙盒裏面的任何信息在App卸載的時候也會被系統刪除(以後安卓手機上再也不用安裝那些垃圾清理軟件了);那問題來了,如果想保存圖片怎麼辦 ? 畢竟有些美好的事物我想留下來啊 ~  恩,Google 爲我們準備了公共存儲空間 MediaStore ;如果一直按照Google的要求來開發,對這個應該不會陌生,只要將沙盒裏面的文件保存到MedaStore中去,那麼就可以長期的保存下來. 如果Android只有沙盒和公共存儲空間的話那和IOS就一樣了.但是安卓就是半吊子,安卓還可以間接的訪問別的應用的沙盒,具體怎麼做了? 恩 通過風騷的系統文件瀏覽器(恩 沒有比這個更垃圾的). 好了下面我們依次來說說這三種類型的存儲空間怎麼操作.

    不想那麼快適配Android Q,想等等別人踩過坑了,再過去怎麼辦 ? 恩 ,我們有如下兩種方案解決這個問題.

1.設置 targetSdkVersion < 29
2.如果 targetSdkVersion >=29,請在manifest中 application標籤中 添加android:requestLegacyExternalStorage=“true”;默認是false

上面兩種方案隨便一種,都可以讓App 在Android Q的系統上 分區存儲 方案失效,進而到達延緩app適配 Android Q的時間.

一. 訪問自己的沙盒空間.

private void createFile(){
        /** /storage/emulated/0/Android/data/com.androidqtest/files/Documents/test.txt */
        String filePath = this.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)+"/test.txt";
        File file =new File(filePath);
        if(!file.exists()){
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

在自己應用的沙盒裏面 增刪改查 文件不需要任何權限 ;也可以對File進行任何操作.

二.MediaStore中的資源.

1.讀取MediaStore中的視頻文件

private void readMediaStoreVideos(){
        String [] selectItems = new String[]{
                MediaStore.Video.VideoColumns.DATA,           //file path
                MediaStore.Video.VideoColumns.SIZE,           //file size
                MediaStore.Video.VideoColumns.DISPLAY_NAME    //file name
        };
        Cursor cursor=this.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,selectItems,null,null,null);
        if(cursor!=null){
            while (cursor.moveToNext()){
                /** /storage/emulated/0/Movies/test.mp4 **/
                String path =cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));
                // ...
            }
        }
        if(cursor!=null){
           cursor.close();
        }
    }

其中獲取的path路徑是絕對路徑,可以使用File類進行IO操作.

2.向MediaStore中插入視頻文件.

public static ContentValues getVideoContentValues(Context paramContext, File paramFile, long paramLong) {
        ContentValues localContentValues = new ContentValues();
        localContentValues.put("title", paramFile.getName());
        localContentValues.put("_display_name", paramFile.getName());
        localContentValues.put("mime_type", "video/3gp");
        localContentValues.put("datetaken", Long.valueOf(paramLong));
        localContentValues.put("date_modified", Long.valueOf(paramLong));
        localContentValues.put("date_added", Long.valueOf(paramLong));
        localContentValues.put("_data", paramFile.getAbsolutePath());
        localContentValues.put("_size", Long.valueOf(paramFile.length()));
        return localContentValues;
    }

    private void insertImageToMediaStoreVideo(){

        String filePath = this.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)+"/videoTest.mp4";
        ContentResolver localContentResolver = this.getContentResolver();
        ContentValues localContentValues = getVideoContentValues(this,new File(filePath), System.currentTimeMillis());
        Uri localUri = localContentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, localContentValues);

        try {
            InputStream is = new FileInputStream(new File(filePath));
            OutputStream os = getContentResolver().openOutputStream(localUri);
            byte[] buffer = new byte[4096]; // tweaking this number may increase performance
            int len;
            while ((len = is.read(buffer)) != -1){
                os.write(buffer, 0, len);
            }
            os.flush();
            is.close();
            os.close();
        } catch (Exception e) {

        }

        /** it works when over android 4.3 **/
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, localUri));
    }

以上介紹的對MediaStore中的資源讀取和插入的方法是通用的;音頻,視頻,圖片,都可以通過這樣做. 相信讀者你一定有個疑問?爲何插入完成後還要進行IO操作,將資源拷貝到Movies中去? 恩,如果進行MediaStore的插入操作不進行拷貝操作的話,當你廣播結束後打開系統相冊你會發現,圖片或者視頻是黑色的一塊矩形封面哈~  原因就是系統在相應的公共資源目錄下面找不相應的資源.所以記得MediaStore插入成功後一定要根據成功後返回的Uri進行IO操作.

對於圖片也有簡單的api可以操作,該api內部會進行拷貝操作,如下:

private void insertImageToMediaStore(){
        String filePath = this.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)+"/test.jpg";
        try {
            /** copy the picture into MediaStore return path of MediaStore **/
            String mediaPath = MediaStore.Images.Media.insertImage(this.getContentResolver(),filePath,"test","one");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

三.其他應用沙盒中數據的獲取

比如獲取SdCard中根目錄的資源. 如下調用該代碼打開系統文件瀏覽器.

public void openSystemFileFilter() {

        // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
        // browser.
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

        // Filter to only show results that can be "opened", such as a
        // file (as opposed to a list of contacts or timezones)
        intent.addCategory(Intent.CATEGORY_OPENABLE);

        /** add it if you want to select multiple files **/
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);

        // Filter to show only images, using the image MIME data type.
        // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
        // To search for all documents available via installed storage providers,
        // it would be "*/*".
        intent.setType("*/*");
        startActivityForResult(intent,66);
    }

如下圖,系統文件夾比較醜.  

選擇完成之後,數據會從onActivityResult中回調回來.

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(null!=data){
             /** if single file **/
             Uri uri = data.getData();

             /** if multiple files **/
             ClipData datas=data.getClipData();
             if(datas!=null){
                 for(int i=0;i<datas.getItemCount();i++){
                     Uri itemUri = datas.getItemAt(i).getUri();
                 }
             }

             /** if you want get fd **/
            try {
                ParcelFileDescriptor parcelFileDescriptor=MainActivity.this.getContentResolver().openFileDescriptor(uri,"r");
                int fd = parcelFileDescriptor.detachFd();

            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

如果是單個文件的話直接從Intent中getData獲取Uri,如果是選擇多個的話,通過Intent的getClipData獲得多個Uri值.系統文件瀏覽器比較垃圾一次只能選擇一個文件夾中的所有非文件夾的純文件(不能遞歸選擇,垃圾).還通過Intent傳值,嘿嘿 有經驗的攻城獅是不是嗅到危險的味道.沒錯當數值過大的時候(多選,選的文件較多) Intent就會拋出異常 : TransactionTooLargeException ,有人會說既然都能拿到Uri那我轉化爲url 然後構建File,然後通過File結構,list不行麼? 首先內存卡中這時File你可以構建成功 exist也是true,但是你卻無法對這些File進行IO操作,這就是Android Q的風騷之處, 那如何進行操作了? 如下通過ContentResolver:

        InputStream myInput;
        OutputStream myOutput;
        ParcelFileDescriptor parcelFileDescriptor =null;
        try {
            parcelFileDescriptor  =FaceGroupApplication.getInstance().getContentResolver().openFileDescriptor(inputUri,"r");
            if(parcelFileDescriptor!=null) {
                myInput = new FileInputStream(parcelFileDescriptor.getFileDescriptor());
                myOutput = new FileOutputStream(output);
                byte[] buffer = new byte[10240]; // 10KB
                int length = myInput.read(buffer);
                while (length > 0) {
                    myOutput.write(buffer, 0, length);
                    length = myInput.read(buffer);
                }
                myOutput.flush();
                myInput.close();
                myOutput.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(parcelFileDescriptor!=null){
                try {
                    parcelFileDescriptor.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                parcelFileDescriptor=null;
            }
        }

ParcelFileDescriptor 具有一次性,用完記得close;然後再次用,需ContentResolver打開使用.當然那種detachFd的可以不用管了.

同樣android Q上面會使用IO操作的api都重載了支持FileDescriptor的接口,例如:

parcelFileDescriptor  =Context.getContentResolver().openFileDescriptor(inputUri,"r");
fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);

四.小結

    1. 沙盒和MediaStore中的資源可以隨意進行File的IO操作和訪問,一如以前android上的存儲策略.

    2.其他沙盒裏面的資源,通過系統文件瀏覽器獲取訪問的Uri,然後ContentResolver解析進行IO;切記不要直接用File結構進行IO操作.

五.問題

    通過上面的分析,我們已然知道Sdcard中其他沙盒中的資源無法用其絕對路徑進行訪問.那很多的第三方 C/C++ 庫,需要使用路徑該腫麼搞? ( 比如 著名的音視頻框架 FFmepg 需要視頻絕對路徑纔可以打開視頻 初始化 FormatContext ).

解決方案如下:

1.其他沙盒中的資源插入MediaStore或者拷貝到自己的沙盒中,這樣絕對路徑的訪問方式就可以使用了.

2.使用Fd的方式,將fd值傳入底層,然後底層直接通過fd的值進行資源內容的訪問.(比如 FFmpeg 就是通過自定義AvioContext .進而繞過傳入路徑的方案,從fd中讀取視頻內容,下一篇將着重介此方案).

六.關於我

這是我的個人技術公衆號(CodeEngine),以後的技術文章會在上面推出,方便看官地跌上打發時間.(歡迎大家掃描下方二維碼)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章