背景介紹 Overview
基於SAF框架寫入外置SD卡網上相關資料比較少,現整理一下具體實現方法,如果是訪問主存儲,彈出授權後即可正常寫入,如果是副卡,在Android9.0上必須要使用SAF框架。
本文檔詳細介紹了應用如何使用Storage Access Framework (SAF框架)訪問External SDcard的方法,使得第三方APP或者應用開發者快速集成寫入sd卡方法。本文將採用Android原生ScreenShot截圖功能集成SAF框架爲例,介紹如何打造可以寫入外部SD卡方法
申請權限
首先申請寫入外部SD卡的權限需要在Activity中,由於Screenshot中無Activity,因此需要創建一個透明Activity來獲取Activity的上下文Context
<activity android:name=".screenshot.ScreenshotPermissionsActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:finishOnCloseSystemDialogs="true"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="com.android.intent.action.REQUEST_SCREENSHOT_STORAGE_PERMISSION" />
</intent-filter>
</activity>
在需要寫入T卡的位置先判斷是否有寫入SD卡權限,如果沒有,則啓動權限申請的ScreenshotPermissionsActivity
String rootPath = WriteSDFileUtil.getRootPath(mContext);
if (SaveImageInBackgroundTask.SAVE_SPRD_EXTERNAL_STORAGE && !TextUtils.isEmpty(rootPath)) {
if (WriteSDFileUtil.hasWriteSDPermission(mContext)) {
saveScreenshot2SD(mScreenBitmap);
} else {
WriteSDFileUtil.startPermissionActivity(mContext);
permissonHandler.sendEmptyMessageDelayed(CHECK_PERMISSION, CHECK_DURATION);
}
}
getRootPath方法如下:
public static String getRootPath(Context context) {
String rootPath = null;
StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
List<StorageVolume> volumes = storageManager.getStorageVolumes();
for (StorageVolume volume : volumes) {
File volumePath = volume.getPathFile();
if (!volume.isPrimary() && volumePath != null &&
Environment.getExternalStorageState(volumePath).equals(Environment.MEDIA_MOUNTED)
&& !volumePath.toString().contains(STORAGE_PATH_EMULATED)) {
rootPath = volumePath.toString();
}
}
Log.i(TAG, "getRootPath rootPath: " + rootPath);
return rootPath;
}
hasWriteSDPermission方法如下:
public static boolean hasWriteSDPermission(Context context) {
return StorageUtil.getInstance().getCurrentAccessUri(context.getContentResolver()) != null;
}
通過getCurrentAccessUri來判斷是否有寫SD卡的權限,如果getCurrentAccessUri獲取的URI爲null則無寫入SD卡的權限:
public Uri getCurrentAccessUri(ContentResolver contentResolver) {
List<UriPermission> uriPermissions = contentResolver.getPersistedUriPermissions();
Log.i(TAG, "getCurrentAccessUri exactStorageName ");
for (UriPermission permission : uriPermissions) {
Log.i(TAG, "getCurrentAccessUri permission: " + permission.toString());
return permission.getUri();
}
Log.i(TAG, "getCurrentAccessUri return null");
return null;
}
ScreenshotPermissionsActivity中權限申請代碼:
// for external storage access permission
private void requestScopedDirectoryAccess() {
int requestCode = -1;
StorageManager storageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
List<StorageVolume> volumes = storageManager.getStorageVolumes();
Log.i(TAG, "requestScopedDirectoryAccess storagePath: " + WriteSDFileUtil.STORAGE_PATH_EMULATED);
for (StorageVolume volume : volumes) {
File volumePath = volume.getPathFile();
Log.i(TAG, "requestScopedDirectoryAccess volumePath: " + volumePath);
if (!volume.isPrimary() && volumePath != null &&
Environment.getExternalStorageState(volumePath).equals(Environment.MEDIA_MOUNTED)
&& !volumePath.toString().contains(WriteSDFileUtil.STORAGE_PATH_EMULATED)) {
mRootPath = volumePath.toString();
Log.i(TAG, "really createAccessIntent for mRootPath: " + mRootPath);
final Intent intent = volume.createAccessIntent(null);
Log.i(TAG, "really createAccessIntent for intent: " + intent);
if (intent != null) {
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, "com.android.systemui");
intent.putExtra("screenshot", true);
startActivityForResult(intent, SCOPED_REQUEST_CODE);
Log.i(TAG, "really createAccessIntent for intent: " + intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME));
}
}
}
}
申請後會彈出權限框
此時如果用戶點擊授權按鈕,則會回調ScreenshotPermissionsActivity中的onActivityResult方法:
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(TAG, " requestCode = " + requestCode + " resultCode = " + resultCode
+ " data = " + data);
if (requestCode == SCOPED_REQUEST_CODE) {
if (resultCode == Activity.RESULT_CANCELED) {
Log.d("huasong", "RESULT_CANCELED:");
sendBroadcastAsUser(new Intent("action.screenshot.permissin.deny"), UserHandle.ALL);
ScreenshotPermissionsActivity.this.finish();
} else if (resultCode == Activity.RESULT_OK) {
Log.d(TAG, "yeah!!!!");
Uri uri = data != null ? data.getData() : null;
final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(uri, takeFlags);
ScreenshotPermissionsActivity.this.finish();
}
}
}
在獲取RESULT_OK用戶授權後,需要調用takePersistableUriPermission保存權限uri,否則下次需要重新授權
寫入文件
主要採用WriteSDFileUtil的WriteSDFile方法首先傳入RootPath
String picName = String.format(SaveImageInBackgroundTask.SCREENSHOT_FILE_NAME_TEMPLATE,
new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(System.currentTimeMillis())));
String rootPath = WriteSDFileUtil.getRootPath(mContext);
WriteSDFileUtil.WriteSDFile(mContext, bitmap, rootPath, picName);
WriteSDFile方法:
public static void WriteSDFile(Context context, Bitmap bitmap, String rootPath, String saveName) {
Uri root = StorageUtil.getInstance().getCurrentAccessUri(context.getContentResolver());
Uri folder = root;
try {
String pictureDir = rootPath + File.separator + PICTURE_DIR;
String screenshotDir = pictureDir + File.separator + SCREENSHOT_DIR;
String saveFileName = screenshotDir + File.separator + saveName;
if (new File(pictureDir).exists()) {
folder = SafTools.getDocumentFileByPath(context, root, pictureDir).getUri();
} else {
folder = DocumentsContract.createDocument(context.getContentResolver(), root,
DocumentsContract.Document.MIME_TYPE_DIR, PICTURE_DIR);
}
if (new File(screenshotDir).exists()) {
folder = SafTools.getDocumentFileByPath(context, folder, screenshotDir).getUri();
} else {
folder = DocumentsContract.createDocument(context.getContentResolver(), folder,
DocumentsContract.Document.MIME_TYPE_DIR, SCREENSHOT_DIR);
}
Uri file = SafTools.createDocument(context.getContentResolver(), folder, new File(saveFileName), MIME_IMAGE);
try {
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(file, "w");
FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
fileOutputStream.write(getBytesByBitmaps(bitmap));
fileOutputStream.close();
pfd.close();
} catch (Exception e) {
Log.d(TAG, "exception:", e);
}
} catch (Exception e) {
}
}
SAFTools中createDocument方法:
/**
* Use tarParUri to create this target file. If need to create lots of documents, not suggest.
* @param resolver
* @param tarParUri
* @param target
* @param mimeType
* @return
*/
public static Uri createDocument(ContentResolver resolver, Uri tarParUri, File target, String mimeType){
Uri result = null;
if(tarParUri != null){
try{
result = DocumentsContract.createDocument(resolver, tarParUri, mimeType, target.getName());
} catch (Exception e) {
result = null;
Log.e(TAG, "createDocument failed! Exception:"+ e);
}
}
if(result == null){
Log.d(TAG,"createDocument failed!");
}
return result;
}
修改DocumentUI代碼
由於權限框拒絕後,下次需要重新彈出,且不可被用戶選擇“拒絕後不再提示”功能,需要修改DocumentUI彈出框的“不再提示”功能不可見,修改方式如下:
packages\apps\DocumentsUI\com\android\documentsui\ScopedAccessActivity.java
private static boolean mIsForScreenshot;
mIsForScreenshot = intent.getBooleanExtra("screenshot", false);
Log.d(TAG, "isForScreenshot:" + mIsForScreenshot);
if (getScopedAccessPermissionStatus(context, mActivity.getCallingPackage(),
mVolumeUuid, directoryName) == PERMISSION_ASK_AGAIN) {
mDontAskAgain.setVisibility(View.VISIBLE);
mDontAskAgain.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(!isChecked);
}
});
}
if (mIsForScreenshot) {
mDontAskAgain.setVisibility(View.GONE);
mDialog.setCanceledOnTouchOutside(false);
}
上面的代碼已經放入到如下鏈接:
源碼地址
如果需要請自行前往下載
謝謝