漫長的假期,在家整理了一下Android 10的適配內容。由於適配篇的篇幅問題,就將這一部本單獨出來,也先放出來。java
Android 4.4 就引入了存儲訪問框架 (SAF)。藉助 SAF,用戶可輕鬆在其全部首選文檔存儲提供程序中瀏覽並打開文檔、圖像及其餘文件。用戶可經過易用的標準界面,以統一方式在全部應用和提供程序中瀏覽文件,以及訪問最近使用的文件。android
SAF 提供的部分功能:git
雖然說早在Android 4.4就已經引入了,可是我卻從未使用過。。。然而在適配Android 10中它倒是一個沒法忽略的存在。由於Android 10的外部存儲訪問限制,咱們沒法像之前同樣自由的操做文件。SAF就是應對這一限制的方法之一。github
使用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 後,就能夠對其執行任何操做。
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;
}
複製代碼
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();
}
}
}
複製代碼
使用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失效。
有了受權就有撤銷受權,使用releasePersistableUriPermission
或revokeUriPermission
方法就能夠實現權限的撤銷。
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。能夠去自行查看體驗。