SAF(Storage Access Framework)使用攻略

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

1.介紹

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

SAF 提供的部分功能:git

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

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

2.使用

選擇文件

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

//經過系統的文件瀏覽器選擇一個文件
    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查詢文件信息:app

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文件爲例):ide

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);
    }
複製代碼

交互頁面以下:ui

在這裏插入圖片描述

讀取文件

得到文件的 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.參考

相關文章
相關標籤/搜索