Android9.0中應用如何通過SAF框架寫入外置SD卡

背景介紹 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);
}

上面的代碼已經放入到如下鏈接:
源碼地址

如果需要請自行前往下載

謝謝

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