背景介绍 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);
}
上面的代码已经放入到如下链接:
源码地址
如果需要请自行前往下载
谢谢