
本文档旨在指导开发者如何在 Android 的 Scoped Storage 环境下,通过 Storage Access Framework (SAF) 读取外部存储特定文件夹中的文件。Scoped Storage 是 Android 10 (API level 29) 引入的存储机制,旨在提高用户隐私和数据安全。本文将提供详细的代码示例,帮助开发者理解 SAF 的使用方法,并解决在 Scoped Storage 中访问特定目录的问题。
Scoped Storage 限制了应用对外部存储的直接访问,应用只能访问自身的特定目录以及用户明确授予访问权限的目录。这提高了用户数据的安全性,防止应用未经授权访问其他应用的数据。
SAF 是一种允许用户选择特定文件或目录并授予应用访问权限的机制。通过 SAF,应用可以安全地访问外部存储,而无需请求广泛的存储权限。
以下是通过 SAF 读取特定文件夹的步骤:
检查 Android 版本: 确保设备运行的是 Android 10 (API level 29) 或更高版本,因为 Scoped Storage 在此版本中强制执行。
创建 Check 类 (Java): 此类的作用是构建一个 Intent,请求用户选择一个目录。它利用 StorageManager 获取主存储卷,并设置初始 URI 为指定的子目录。
package com.axanor.saf_sample;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.storage.StorageManager;
import android.util.Log;
public class Check {
Context context;
private String TAG="SOMNATH";
public Check(Context context) {
this.context = context;
}
public boolean ch(){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
Intent intent = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();
//String startDir = "Android";
//String startDir = "Download"; // Not choosable on an Android 11 device
//String startDir = "DCIM";
//String startDir = "DCIM/Camera"; // replace "/", "%2F"
//String startDir = "DCIM%2FCamera";
String startDir = "Documents";
Uri uri = intent.getParcelableExtra("android.provider.extra.INITIAL_URI");
String scheme = uri.toString();
Log.d(TAG, "INITIAL_URI scheme: " + scheme);
scheme = scheme.replace("/root/", "/document/");
scheme += "%3A" + startDir;
uri = Uri.parse(scheme);
intent.putExtra("android.provider.extra.INITIAL_URI", uri);
Log.d(TAG, "uri: " + uri.toString());
((Activity) context).startActivityForResult(intent, 12123);
return true;
}
else{
return false;
}
}
}代码解释:
里面有2个文件夹。其中这个文件名是:finishing,是我项目还没有请求后台的数据的模拟写法。请求后台数据之后,瀑布流的js有一点点变化,放在文件名是:finished。变化在于需要穿参数到后台,和填充的内容都用后台的数据填充。看自己项目需求来。由于chrome模拟器是不允许读取本地文件json的,所以如果你要进行测试,在hbuilder打开项目就可以看到效果啦,或者是火狐浏览器。
92
package com.axanor.saf_sample
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
class StorageAccess {
val activity:Activity;
val LOGTAG = "SOMNATH"
val REQUEST_CODE = 12123
constructor(activity: Activity) {
this.activity = activity
}
public fun openDocumentTree() {
val check = Check(activity)
val uriString = SpUtil.getString(SpUtil.FOLDER_URI, "")
when {
uriString == "" -> {
Log.w(LOGTAG, "uri not stored")
if (!check.ch()){
askPermission()
}
}
arePermissionsGranted(uriString) -> {
makeDoc(Uri.parse(uriString))
}
else -> {
Log.w(LOGTAG, "uri permission not stored")
if (!check.ch()){
askPermission()
}
}
}
}
// this will present the user with folder browser to select a folder for our data
public fun askPermission() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
activity.startActivityForResult(intent, REQUEST_CODE)
}
public fun makeDoc(dirUri: Uri) {
val dir = DocumentFile.fromTreeUri(activity, dirUri)
if (dir == null || !dir.exists()) {
//the folder was probably deleted
Log.e(LOGTAG, "no Dir")
//according to Commonsware blog, the number of persisted uri permissions is limited
//so we should release those we cannot use anymore
//https://commonsware.com/blog/2020/06/13/count-your-saf-uri-permission-grants.html
releasePermissions(dirUri)
//ask user to choose another folder
Toast.makeText(activity,"Folder deleted, please choose another!", Toast.LENGTH_SHORT).show()
openDocumentTree()
} else {
val file = dir.createFile("*/txt", "test.txt")
if (file != null && file.canWrite()) {
Log.d(LOGTAG, "file.uri = ${file.uri.toString()}")
alterDocument(file.uri)
} else {
Log.d(LOGTAG, "no file or cannot write")
//consider showing some more appropriate error message
Toast.makeText(activity,"Write error!", Toast.LENGTH_SHORT).show()
}
}
}
public fun releasePermissions(uri: Uri) {
val flags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
activity.contentResolver.releasePersistableUriPermission(uri,flags)
//we should remove this uri from our shared prefs, so we can start over again next time
SpUtil.storeString(SpUtil.FOLDER_URI, "")
}
//Just a test function to write something into a file, from https://developer.android.com
//Please note, that all file IO MUST be done on a background thread. It is not so in this
//sample - for the sake of brevity.
public fun alterDocument(uri: Uri) {
try {
activity.contentResolver.openFileDescriptor(uri, "w")?.use { parcelFileDescriptor ->
FileOutputStream(parcelFileDescriptor.fileDescriptor).use {
it.write(
("String written at ${System.currentTimeMillis()}\n")
.toByteArray()
)
Toast.makeText(activity,"File Write OK!", Toast.LENGTH_SHORT).show()
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/pdf"
// Optionally, specify a URI for the file that should appear in the
// system file picker when it loads.
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
activity.startActivityForResult(intent, 2)
}
}
} catch (e: FileNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
public fun arePermissionsGranted(uriString: String): Boolean {
// list of all persisted permissions for our app
val list = activity.contentResolver.persistedUriPermissions
for (i in list.indices) {
val persistedUriString = list[i].uri.toString()
//Log.d(LOGTAG, "comparing $persistedUriString and $uriString")
if (persistedUriString == uriString && list[i].isWritePermission && list[i].isReadPermission) {
//Log.d(LOGTAG, "permission ok")
return true
}
}
return false
}
}代码解释:
StorageAccess access = new StorageAccess(this); access.openDocumentTree();
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 12123 && resultCode == Activity.RESULT_OK) {
val uri = data?.data
if (uri != null) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
applicationContext.contentResolver.takePersistableUriPermission(uri, flags)
SpUtil.storeString(SpUtil.FOLDER_URI, uri.toString())
val storageAccess = StorageAccess(this)
storageAccess.makeDoc(uri)
}
}
}代码解释:
package com.axanor.saf_sample;
import android.content.Context;
import android.content.SharedPreferences;
public class SpUtil {
private static final String PREF_NAME = "my_prefs";
public static final String FOLDER_URI = "folder_uri";
public static void storeString(String key, String value) {
SharedPreferences prefs = App.getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(key, value);
editor.apply();
}
public static String getString(String key, String defaultValue) {
SharedPreferences prefs = App.getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
return prefs.getString(key, defaultValue);
}
// 假设你有一个 App 类,可以提供全局的 Context
public static class App {
private static Context context;
public static void setContext(Context c) {
context = c;
}
public static Context getContext() {
return context;
}
}
}代码解释:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
注意: MANAGE_EXTERNAL_STORAGE 权限通常需要经过 Google Play 审核,因此请谨慎使用,并确保你的应用确实需要访问所有文件。 如果没有特殊需求,请尽量避免使用此权限,而选择 SAF。
通过使用 Storage Access Framework (SAF),开发者可以在 Android Scoped Storage 环境下安全地访问外部存储的特定文件夹。本文档提供了详细的代码示例和步骤,帮助开发者理解 SAF 的使用方法,并解决在 Scoped Storage 中访问特定目录的问题。 请记住,提供良好的用户体验、进行适当的异常处理、并在后台线程中执行文件 I/O 操作是至关重要的。
以上就是如何在 Android Scoped Storage 中读取特定文件夹的文件的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号