SAF(Storage Access Framework)使用攻略

漫長的假期,在家整理了一下Android 10的適配內容。因爲適配篇的篇幅問題,就將這一部本單獨出來,也先放出來。

1.介紹

Android 4.4 就引入了存儲訪問框架 (SAF)。藉助 SAF,用戶可輕鬆在其所有首選文檔存儲提供程序中瀏覽並打開文檔、圖像及其他文件。用戶可通過易用的標準界面,以統一方式在所有應用和提供程序中瀏覽文件,以及訪問最近使用的文件。

SAF 提供的部分功能:

  • 讓用戶瀏覽所有文檔提供程序的內容,而不僅僅是單個應用的內容。
  • 讓您的應用獲得對文檔提供程序所擁有文檔的長期、持續性訪問權限。用戶可通過此訪問權限添加、編輯、保存和刪除提供程序上的文件。
  • 支持多個用戶帳戶和臨時根目錄,如只有在插入驅動器後纔會出現的 USB 存儲提供程序。

雖說早在Android 4.4就已經引入了,但是我卻從未使用過。。。然而在適配Android 10中它卻是一個無法忽略的存在。因爲Android 10的外部存儲訪問限制,我們無法像以前一樣自由的操作文件。SAF就是應對這一限制的方法之一。

2.使用

選擇文件

使用Intent.ACTION_OPEN_DOCUMENT可以調起文件選擇頁面,選擇一個文件。我以選擇圖片文件爲例:

	//通過系統的文件瀏覽器選擇一個文件
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    //篩選,只顯示可以“打開”的結果,如文件(而不是聯繫人或時區列表)
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    //過濾只顯示圖像類型文件
    intent.setType("image/*");
    startActivityForResult(intent, REQUEST_CODE_FOR_SINGLE_FILE);

文件選擇頁面如下(系統MIUI 11):

在這裏插入圖片描述

onActivityResult獲取文件Uri,同時也可以通過ContentResolver查詢文件信息:

private final String[] IMAGE_PROJECTION = {
            MediaStore.Images.Media.DISPLAY_NAME,
            MediaStore.Images.Media.SIZE,
            MediaStore.Images.Media._ID };

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        Uri uri = null;
        if (resultData != null) {
        	// 獲取選擇文件Uri
            uri = resultData.getData();
            // 獲取圖片信息
            Cursor cursor = this.getContentResolver()
                .query(uri, IMAGE_PROJECTION, null, null, null, null);

            if (cursor != null && cursor.moveToFirst()) {
                String displayName = cursor.getString(cursor.getColumnIndexOrThrow(IMAGE_PROJECTION[0]));
                String size = cursor.getString(cursor.getColumnIndexOrThrow(IMAGE_PROJECTION[1]));
                Log.i(TAG, "Uri: " + uri.toString());
                Log.i(TAG, "Name: " + displayName);
                Log.i(TAG, "Size: " + size);
            }
            cursor.close();
        }
    }
}

創建文件

這部分的用法我暫時也只在淘寶App -> 商品評論 -> 保存評論圖片的地方看到過。有興趣的可以去試試。

具體用法(我以創建txt文件爲例):

	public void createFile() {
        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        // 文件類型
        intent.setType("text/plain");
        // 文件名稱
        intent.putExtra(Intent.EXTRA_TITLE, System.currentTimeMillis() + ".txt");
        startActivityForResult(intent, WRITE_REQUEST_CODE);
    }

交互頁面如下:

在這裏插入圖片描述

讀取文件

獲得文件的 Uri 後,就可以對其執行任何操作。

  1. Bitmap
	private Bitmap getBitmapFromUri(Uri uri) throws IOException {
    	ParcelFileDescriptor parcelFileDescriptor =
            	getContentResolver().openFileDescriptor(uri, "r");
    	FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
    	Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
    	parcelFileDescriptor.close();
    	return image;
	}
  1. 獲取 InputStream
	private String readTextFromUri(Uri uri) throws IOException {
    	StringBuilder stringBuilder = new StringBuilder();
    	try (InputStream inputStream = getContentResolver().openInputStream(uri);
            BufferedReader reader = new BufferedReader(
            new InputStreamReader(Objects.requireNonNull(inputStream)))) {
        	String line;
        	while ((line = reader.readLine()) != null) {
            	stringBuilder.append(line);
        	}
    	}
    	return stringBuilder.toString();
	}

修改文件

	private void alterDocument(Uri uri) {
        if (uri != null) {
            OutputStream outputStream = null;
            try {
                // 獲取 OutputStream
                outputStream = getContentResolver().openOutputStream(uri);
                outputStream.write("Storage Access Framework Example".getBytes(StandardCharsets.UTF_8));
            } catch (IOException e) {
                Toast.makeText(this, "修改文件失敗!", Toast.LENGTH_SHORT).show();
            } finally {
                if (outputStream != null) {
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        e.fillInStackTrace();
                    }
                }
            }
        } 
    }

	private void alterDocument(Uri uri) {
    	try {
        	ParcelFileDescriptor pfd = getContentResolver().
                openFileDescriptor(uri, "w");
        	FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        	fileOutputStream.write(("Storage Access Framework Example").getBytes());
        	fileOutputStream.close();
        	pfd.close();
    	} catch (FileNotFoundException e) {
        	e.printStackTrace();
    	} catch (IOException e) {
        	e.printStackTrace();
    	}
	}

刪除文件

使用DocumentsContract.deleteDocument 方法進行刪除。

	public void deleteFile(Uri uri) {
        if (uri != null) {
            try {
                DocumentsContract.deleteDocument(getContentResolver(), uri);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        } 
    }

選擇目錄(Android 5.0以上支持)

使用Intent.ACTION_OPEN_DOCUMENT_TREE可以調起文件目錄選擇頁面,選擇一個目錄,並將其子文件夾的讀寫權限授予APP。

	private void selectDir() {
        // 用戶可以選擇任意文件夾,將它及其子文件夾的讀寫權限授予APP。
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
        startActivityForResult(intent, REQUEST_CODE_FOR_DIR);
    }

交互頁面如下:
在這裏插入圖片描述
onActivityResult獲取目錄的Uri,並創建DocumentFile來進行文件操作:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    if (requestCode == REQUEST_CODE_FOR_DIR && resultCode == Activity.RESULT_OK) {
        Uri uriTree = null;
    	if (data != null) {
        	uriTree = data.getData();
    	}
    	if (uriTree != null) {
        	// 創建所選目錄的DocumentFile,可以使用它進行文件操作
        	DocumentFile root = DocumentFile.fromTreeUri(this, uriTree);
        	// 比如使用它創建文件夾
        	DocumentFile dir = root.createDirectory(”Test“);
   		}
    }
}

當然每次這樣選擇授權會很麻煩,所以我們也可以在首次授權時保存獲取的目錄權限:

	// 獲取權限
    final int takeFlags = resultData.getFlags()
			& (Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    getContentResolver().takePersistableUriPermission(uri, takeFlags);
	// 保存獲取的目錄權限
    SharedPreferences sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE);
    SharedPreferences.Editor editor = sp.edit();
    editor.putString("uriTree", uri.toString());
    editor.apply();

使用時從SharedPreferences獲取uriTree,不存在或是無權限則重新授權:

	SharedPreferences sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE);
	String uriTree = sp.getString("uriTree", "");
	if (TextUtils.isEmpty(uriTree)) {
    	// 重新授權
	} else {
    	try {
        	Uri uri = Uri.parse(uriTree);
        	final int takeFlags = getIntent().getFlags()
        	        & (Intent.FLAG_GRANT_READ_URI_PERMISSION
                	| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        	getContentResolver().takePersistableUriPermission(uri, takeFlags);
        	DocumentFile root = DocumentFile.fromTreeUri(this, uri);
    	} catch (SecurityException e) {
        	// 重新授權
    	}
	}

上面代碼中使用到的takePersistableUriPermission方法是爲了檢查最新的數據。防止另一個應用可能刪除或修改了文件導致Uri失效。

有了授權就有撤銷授權,使用releasePersistableUriPermissionrevokeUriPermission方法就可以實現權限的撤銷。

	public void releasePermission(View view) {
        SharedPreferences sp = getSharedPreferences("DirPermission", Context.MODE_PRIVATE);
        String uriTree = sp.getString("uriTree", "");
        if (!TextUtils.isEmpty(uriTree)) {
            Uri uri = Uri.parse(uriTree);
                final int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION
                		| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
                
            getContentResolver().releasePersistableUriPermission(uri, takeFlags);
            // 或
            this.revokeUriPermission(uri, takeFlags);
            // 重啓纔會生效,所以可以清除uriTree
            SharedPreferences.Editor editor = sp.edit();
            editor.putString("uriTree", "");
            editor.apply();
        } 
    }

或者在應用設置頁面點擊取消訪問權限手動刪除(MIUI 11 上未發現此按鈕):

在這裏插入圖片描述

本篇都是具體場景的的使用示例,完整的代碼我已上傳GitHub。可以去自行查看體驗。

3.參考

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