某些应用程序如何在没有 root 的情况下访问 Android 11 上的“.../Android/...”子文件夹的内容?
How do some apps reach the contents of ".../Android/..." sub-folders on Android 11 without root?
背景
有 various storage restrictions on Android 10 and 11, which also includes a new permission (MANAGE_EXTERNAL_STORAGE) 访问所有文件(但它不允许访问真正的所有文件),而以前的存储权限已减少为只授予对媒体文件的访问权限:
- 应用程序可以自由访问“媒体”子文件夹。
- 应用程序永远无法访问“数据”子文件夹,尤其是内容。
- 对于“obb”文件夹,如果允许应用程序安装应用程序,它可以访问它(将文件复制到那里)。否则不行。
- 使用 USB 或 root,您仍然可以访问它们,作为最终用户,您可以通过内置的文件管理器应用程序“文件”访问它们。
问题
我注意到一个应用程序以某种方式克服了这个限制(here) called "X-plore”:一旦你进入“Android/data”文件夹,它会要求你授予访问权限(直接使用 SAF,不知何故), 当你授予它时,你可以访问“Android”文件夹中所有文件夹中的所有内容。
这意味着可能仍然有办法到达它,但问题是由于某种原因我无法制作同样的样本。
我发现并尝试过的
似乎这个应用程序的目标是 API 29 (Android 10),而且它还没有使用新的权限,它有标志 requestLegacyExternalStorage。我不知道他们使用的相同技巧在针对 API 30 时是否有效,但我可以说在我的情况下,运行 在 Pixel 4 上使用 Android 11,效果很好.
所以我也尝试这样做:
我制作了一个示例 POC,目标是 Android API 29,已授予(各种)存储权限,包括遗留标志。
我试图请求直接访问“Android”文件夹(基于here),不幸的是由于某些原因它没有工作(继续到 DCIM 文件夹,不知道为什么):
val androidFolderDocumentFile = DocumentFile.fromFile(File(primaryVolume.directory!!, "Android"))
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
.putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidFolderDocumentFile.uri)
startActivityForResult(intent, 1)
我尝试了各种标志组合。
启动应用程序时,当我自己手动到达“Android”文件夹时,因为这效果不佳,我授予了对该文件夹的访问权限,就像在另一个文件夹上一样应用
获取结果时,尝试获取路径中的文件和文件夹,但获取失败:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d("AppLog", "resultCode:$resultCode")
val uri = data?.data ?: return
if (!DocumentFile.isDocumentUri(this, uri))
return
grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val fullPathFromTreeUri = FileUtilEx.getFullPathFromTreeUri(this, uri) // code for this here:
val documentFile = DocumentFile.fromTreeUri(this, uri)
val listFiles: Array<DocumentFile> = documentFile!!.listFiles() // this returns just an array of a single folder ("media")
val androidFolder = File(fullPathFromTreeUri)
androidFolder.listFiles()?.forEach {
Log.d("AppLog", "${it.absoluteFile} children:${it.listFiles()?.joinToString()}") //this does find the folders, but can't reach their contents
}
Log.d("AppLog", "granted uri:$uri $fullPathFromTreeUri")
}
所以使用 DocumentFile.fromTreeUri 我仍然只能得到没用的“媒体”文件夹,使用文件 class 我只能看到还有“数据”和“obb”文件夹,但仍然无法访问他们的内容...
所以这根本没用。
后来我发现了另一个使用这个技巧的应用程序,叫做“MiXplorer”。在此应用程序上,它无法直接请求“Android”文件夹(也许它甚至没有尝试),但一旦您允许,它就会授予您对它及其子文件夹的完全访问权限。而且,它的目标是 API 30,所以这意味着它不能正常工作,因为你的目标是 API 29.
我注意到(有人写信给我)通过对代码进行一些更改,我可以请求分别访问每个子文件夹(意味着对“数据”的请求和对“obb”的新请求),但这不是我在这里看到的,而是应用程序所做的。
意思是,为了进入“Android”文件夹,我使用这个 Uri 作为 Intent.EXTRA_INITIAL_URI 的参数:
val androidUri=Uri.Builder().scheme("content").authority("com.android.externalstorage.documents")
.appendEncodedPath("tree").appendPath("primary:").appendPath("document").appendPath("primary:Android").build()
但是,一旦您可以访问它,您将无法从中获取文件列表,不能通过文件,也不能通过 SAF。
但是,正如我所写,奇怪的是,如果您尝试类似的操作,而不是转到“Android/data”,您将能够获取其内容:
val androidDataUri=Uri.Builder().scheme("content").authority("com.android.externalstorage.documents")
.appendEncodedPath("tree").appendPath("primary:").appendPath("document").appendPath("primary:Android/data").build()
问题
- 我怎样才能直接向“Android”文件夹请求一个 Intent,这样我才能真正访问它,并让我获得子文件夹及其内容?
- 还有其他选择吗?也许使用 adb and/or root,我可以授予 SAF 访问这个特定文件夹的权限?
好吧,我试过这段代码,它适用于 Android API 29,Samsung Galaxy 20FE:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_STORAGE_ACCESS) {
Uri treeUri = null;
// Get Uri from Storage Access Framework.
treeUri = data.getData();
// Persist URI in shared preference so that you can use it later.
// Use your own framework here instead of PreferenceUtil.
MySharedPreferences.getInstance(null).setFileURI(treeUri);
// Persist access permissions.
final int takeFlags = data.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
createDir(DIR_PATH);
finish();
}
}
private void createDir(String path) {
Uri treeUri = MySharedPreferences.getInstance(null).getFileURI();
if (treeUri == null) {
return;
}
// start with root of SD card and then parse through document tree.
DocumentFile document = DocumentFile.fromTreeUri(getApplicationContext(), treeUri);
document.createDirectory(path);
}
我是通过单击按钮调用它的:
Button btnLinkSd = findViewById(R.id.btnLinkSD);
btnLinkSd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
triggerStorageAccessFramework();
}
});
在 UI 中,我按“显示内部存储”,导航到 Android 目录并按允许。之后,在调试中,如果我尝试列出 android 下的所有文件,我将获得 Data 中所有目录的列表。如果这就是你要找的。
最后,调试结果:
这是它在 X-plore 中的工作方式:
启用时 Build.VERSION.SDK_INT>=30
,
[内部存储]/Android/data不可访问,javaFile.canRead()
或File.canWrite()
returnsfalse,所以我们需要为里面的文件切换到备用文件系统文件夹(也可能是 obb)。
您已经知道存储访问框架是如何工作的,所以我将详细说明具体需要做什么。
您致电 ContentResolver.getPersistedUriPermissions()
以了解您是否已保存此文件夹的权限。最初你没有它,所以你请求用户许可:
要请求访问权限,请使用 startActivityForResult
和 Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).putExtra(DocumentsContract.EXTRA_INITIAL_URI, DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Android"))
在这里你用 EXTRA_INITIAL_URI
设置选择器将直接在主存储上的 Android 文件夹上启动,因为我们想要访问 Android 文件夹。当您的应用程序将以 API30 为目标时,选择器将不允许选择存储根目录,并且通过获得对 Android 文件夹的权限,您可以同时使用 data
和 obb
里面的文件夹,需要一个权限。
当用户点击 2 次确认后,在 onActivityResult
中,您将在数据中得到 Uri,它应该是 content://com.android.externalstorage.documents/tree/primary%3AAndroid
。进行必要的检查以验证用户是否确认了正确的文件夹。然后调用contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION)
保存权限,就可以了。
所以我们回到 ContentResolver.getPersistedUriPermissions()
,其中包含已授予权限的列表(可能还有更多),您在上面授予的权限如下所示:UriPermission {uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid, modeFlags=3}
(相同你在 onActivityResult
) 中得到的 Uri。从 getPersistedUriPermissions
迭代列表以找到感兴趣的 uri,如果找到则使用它,否则请求用户授予。
现在您想使用此“树”uri 和 Android 文件夹内文件的相对路径来处理 ContentResolver
和 DocumentsContract
。以下是列出 data
文件夹的示例:
data/
是相对于授予的“树”uri 的路径。使用 DocumentsContract.buildChildDocumentsUriUsingTree()
(列出文件)或 DocumentsContract.buildDocumentUriUsingTree()
(处理单个文件)构建最终的 uri,例如:DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri), DocumentsContract.getTreeDocumentId(treeUri)+"/data/")
,你会得到 uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid/document/primary%3AAndroid%2Fdata%2F/children
适合列出数据文件夹中的文件。现在调用 ContentResolver.query(uri, ...)
并处理 Cursor
中的数据以获取文件夹列表。
您使用其他 SAF 功能的方式类似于您可能已经知道的 read/write/rename/move/delete/create,使用 ContentResolver
或 DocumentsContract
的方法。
一些细节:
- 不需要
android.permission.MANAGE_EXTERNAL_STORAGE
- 它适用于目标 API 29 或 30
- 它仅适用于主存储,不适用于外部 SD 卡
- 对于数据文件夹中的所有文件,您需要使用 SAF(java 文件不起作用),只需使用相对于 Android 文件夹的文件层次结构
- 将来 Google 可能会在他们的“安全”意图中修补这个漏洞,并且在某些安全更新后这可能无法工作
编辑:示例代码,基于 Cheticamp Github sample。该示例显示了“Android”文件夹的每个子文件夹的内容(和文件数):
class MainActivity : AppCompatActivity() {
private val handleIntentActivityResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode != Activity.RESULT_OK)
return@registerForActivityResult
val directoryUri = it.data?.data ?: return@registerForActivityResult
contentResolver.takePersistableUriPermission(
directoryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
if (checkIfGotAccess())
onGotAccess()
else
Log.d("AppLog", "you didn't grant permission to the correct folder")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
val openDirectoryButton = findViewById<FloatingActionButton>(R.id.fab_open_directory)
openDirectoryButton.setOnClickListener {
openDirectory()
}
}
private fun checkIfGotAccess(): Boolean {
return contentResolver.persistedUriPermissions.indexOfFirst { uriPermission ->
uriPermission.uri.equals(androidTreeUri) && uriPermission.isReadPermission && uriPermission.isWritePermission
} >= 0
}
private fun onGotAccess() {
Log.d("AppLog", "got access to Android folder. showing content of each folder:")
@Suppress("DEPRECATION")
File(Environment.getExternalStorageDirectory(), "Android").listFiles()?.forEach { androidSubFolder ->
val docId = "$ANDROID_DOCID/${androidSubFolder.name}"
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(androidTreeUri, docId)
val contentResolver = this.contentResolver
Log.d("AppLog", "content of:${androidSubFolder.absolutePath} :")
contentResolver.query(childrenUri, null, null, null)
?.use { cursor ->
val filesCount = cursor.count
Log.d("AppLog", "filesCount:$filesCount")
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val mimeIndex = cursor.getColumnIndex("mime_type")
while (cursor.moveToNext()) {
val displayName = cursor.getString(nameIndex)
val mimeType = cursor.getString(mimeIndex)
Log.d("AppLog", " $displayName isFolder?${mimeType == DocumentsContract.Document.MIME_TYPE_DIR}")
}
}
}
}
private fun openDirectory() {
if (checkIfGotAccess())
onGotAccess()
else {
val primaryStorageVolume = (getSystemService(STORAGE_SERVICE) as StorageManager).primaryStorageVolume
val intent =
primaryStorageVolume.createOpenDocumentTreeIntent().putExtra(EXTRA_INITIAL_URI, androidUri)
handleIntentActivityResult.launch(intent)
}
}
companion object {
private const val ANDROID_DOCID = "primary:Android"
private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
private val androidUri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
private val androidTreeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
}
}
"Java 版本测试 Android 11"
这会将文件从 assets 文件夹复制到 android/data/xxx
中的任何目录
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class MainActivity extends AppCompatActivity
{
private final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents";
private final String ANDROID_DOCID =
"primary:Android/data/xxxxFolderName";
Uri uri;
Uri treeUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button b=findViewById(R.id.ok);
uri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
ANDROID_DOCID
);
treeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
ANDROID_DOCID
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
openDirectory();
}
}
private Boolean checkIfGotAccess() {
List<UriPermission> permissionList = getContentResolver().getPersistedUriPermissions();
for (int i = 0; i < permissionList.size(); i++) {
UriPermission it = permissionList.get(i);
if (it.getUri().equals(treeUri) && it.isReadPermission())
return true;
}
return false;
}
@RequiresApi(api = Build.VERSION_CODES.Q)
private void openDirectory() {
if (checkIfGotAccess()) {
copyFile(treeUri);
//return;
}
Intent intent =
getPrimaryVolume().createOpenDocumentTreeIntent()
.putExtra(EXTRA_INITIAL_URI, uri);
ActivityResultLauncher<Intent> handleIntentActivityResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
if (result.getData() == null || result.getData().getData() == null)
return;
Uri directoryUri = result.getData().getData();
getContentResolver().takePersistableUriPermission(
directoryUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
);
if (checkIfGotAccess())
copyFile(treeUri);
else
Log.d("AppLog", "you didn't grant permission to the correct folder");
}
});
handleIntentActivityResult.launch(intent);
}
@RequiresApi(api = Build.VERSION_CODES.N)
private StorageVolume getPrimaryVolume() {
StorageManager sm = (StorageManager) getSystemService(STORAGE_SERVICE);
return sm.getPrimaryStorageVolume();
}
private void copyFile(Uri treeUri) {
AssetManager assetManager = getAssets();
OutputStream out;
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
String extension = "ini";
try {
InputStream inn = assetManager.open("xxxxfileName.ini");
assert pickedDir != null;
DocumentFile existing = pickedDir.findFile("xxxxfileName.ini");
if(existing!=null)
existing.delete();
DocumentFile newFile = pickedDir.createFile("*/" + extension, "EnjoyCJZC.ini");
assert newFile != null;
out = getContentResolver().openOutputStream(newFile.getUri());
byte[] buffer = new byte[1024];
int read;
while ((read = inn.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
inn.close();
out.flush();
out.close();
} catch (Exception fnfe1) {
fnfe1.printStackTrace();
}
}
}
背景
有 various storage restrictions on Android 10 and 11, which also includes a new permission (MANAGE_EXTERNAL_STORAGE) 访问所有文件(但它不允许访问真正的所有文件),而以前的存储权限已减少为只授予对媒体文件的访问权限:
- 应用程序可以自由访问“媒体”子文件夹。
- 应用程序永远无法访问“数据”子文件夹,尤其是内容。
- 对于“obb”文件夹,如果允许应用程序安装应用程序,它可以访问它(将文件复制到那里)。否则不行。
- 使用 USB 或 root,您仍然可以访问它们,作为最终用户,您可以通过内置的文件管理器应用程序“文件”访问它们。
问题
我注意到一个应用程序以某种方式克服了这个限制(here) called "X-plore”:一旦你进入“Android/data”文件夹,它会要求你授予访问权限(直接使用 SAF,不知何故), 当你授予它时,你可以访问“Android”文件夹中所有文件夹中的所有内容。
这意味着可能仍然有办法到达它,但问题是由于某种原因我无法制作同样的样本。
我发现并尝试过的
似乎这个应用程序的目标是 API 29 (Android 10),而且它还没有使用新的权限,它有标志 requestLegacyExternalStorage。我不知道他们使用的相同技巧在针对 API 30 时是否有效,但我可以说在我的情况下,运行 在 Pixel 4 上使用 Android 11,效果很好.
所以我也尝试这样做:
我制作了一个示例 POC,目标是 Android API 29,已授予(各种)存储权限,包括遗留标志。
我试图请求直接访问“Android”文件夹(基于here),不幸的是由于某些原因它没有工作(继续到 DCIM 文件夹,不知道为什么):
val androidFolderDocumentFile = DocumentFile.fromFile(File(primaryVolume.directory!!, "Android"))
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
.putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidFolderDocumentFile.uri)
startActivityForResult(intent, 1)
我尝试了各种标志组合。
启动应用程序时,当我自己手动到达“Android”文件夹时,因为这效果不佳,我授予了对该文件夹的访问权限,就像在另一个文件夹上一样应用
获取结果时,尝试获取路径中的文件和文件夹,但获取失败:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d("AppLog", "resultCode:$resultCode")
val uri = data?.data ?: return
if (!DocumentFile.isDocumentUri(this, uri))
return
grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val fullPathFromTreeUri = FileUtilEx.getFullPathFromTreeUri(this, uri) // code for this here:
val documentFile = DocumentFile.fromTreeUri(this, uri)
val listFiles: Array<DocumentFile> = documentFile!!.listFiles() // this returns just an array of a single folder ("media")
val androidFolder = File(fullPathFromTreeUri)
androidFolder.listFiles()?.forEach {
Log.d("AppLog", "${it.absoluteFile} children:${it.listFiles()?.joinToString()}") //this does find the folders, but can't reach their contents
}
Log.d("AppLog", "granted uri:$uri $fullPathFromTreeUri")
}
所以使用 DocumentFile.fromTreeUri 我仍然只能得到没用的“媒体”文件夹,使用文件 class 我只能看到还有“数据”和“obb”文件夹,但仍然无法访问他们的内容...
所以这根本没用。
后来我发现了另一个使用这个技巧的应用程序,叫做“MiXplorer”。在此应用程序上,它无法直接请求“Android”文件夹(也许它甚至没有尝试),但一旦您允许,它就会授予您对它及其子文件夹的完全访问权限。而且,它的目标是 API 30,所以这意味着它不能正常工作,因为你的目标是 API 29.
我注意到(有人写信给我)通过对代码进行一些更改,我可以请求分别访问每个子文件夹(意味着对“数据”的请求和对“obb”的新请求),但这不是我在这里看到的,而是应用程序所做的。
意思是,为了进入“Android”文件夹,我使用这个 Uri 作为 Intent.EXTRA_INITIAL_URI 的参数:
val androidUri=Uri.Builder().scheme("content").authority("com.android.externalstorage.documents")
.appendEncodedPath("tree").appendPath("primary:").appendPath("document").appendPath("primary:Android").build()
但是,一旦您可以访问它,您将无法从中获取文件列表,不能通过文件,也不能通过 SAF。
但是,正如我所写,奇怪的是,如果您尝试类似的操作,而不是转到“Android/data”,您将能够获取其内容:
val androidDataUri=Uri.Builder().scheme("content").authority("com.android.externalstorage.documents")
.appendEncodedPath("tree").appendPath("primary:").appendPath("document").appendPath("primary:Android/data").build()
问题
- 我怎样才能直接向“Android”文件夹请求一个 Intent,这样我才能真正访问它,并让我获得子文件夹及其内容?
- 还有其他选择吗?也许使用 adb and/or root,我可以授予 SAF 访问这个特定文件夹的权限?
好吧,我试过这段代码,它适用于 Android API 29,Samsung Galaxy 20FE:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_STORAGE_ACCESS) {
Uri treeUri = null;
// Get Uri from Storage Access Framework.
treeUri = data.getData();
// Persist URI in shared preference so that you can use it later.
// Use your own framework here instead of PreferenceUtil.
MySharedPreferences.getInstance(null).setFileURI(treeUri);
// Persist access permissions.
final int takeFlags = data.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
createDir(DIR_PATH);
finish();
}
}
private void createDir(String path) {
Uri treeUri = MySharedPreferences.getInstance(null).getFileURI();
if (treeUri == null) {
return;
}
// start with root of SD card and then parse through document tree.
DocumentFile document = DocumentFile.fromTreeUri(getApplicationContext(), treeUri);
document.createDirectory(path);
}
我是通过单击按钮调用它的:
Button btnLinkSd = findViewById(R.id.btnLinkSD);
btnLinkSd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
triggerStorageAccessFramework();
}
});
在 UI 中,我按“显示内部存储”,导航到 Android 目录并按允许。之后,在调试中,如果我尝试列出 android 下的所有文件,我将获得 Data 中所有目录的列表。如果这就是你要找的。
最后,调试结果:
这是它在 X-plore 中的工作方式:
启用时 Build.VERSION.SDK_INT>=30
,
[内部存储]/Android/data不可访问,javaFile.canRead()
或File.canWrite()
returnsfalse,所以我们需要为里面的文件切换到备用文件系统文件夹(也可能是 obb)。
您已经知道存储访问框架是如何工作的,所以我将详细说明具体需要做什么。
您致电 ContentResolver.getPersistedUriPermissions()
以了解您是否已保存此文件夹的权限。最初你没有它,所以你请求用户许可:
要请求访问权限,请使用 startActivityForResult
和 Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).putExtra(DocumentsContract.EXTRA_INITIAL_URI, DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Android"))
在这里你用 EXTRA_INITIAL_URI
设置选择器将直接在主存储上的 Android 文件夹上启动,因为我们想要访问 Android 文件夹。当您的应用程序将以 API30 为目标时,选择器将不允许选择存储根目录,并且通过获得对 Android 文件夹的权限,您可以同时使用 data
和 obb
里面的文件夹,需要一个权限。
当用户点击 2 次确认后,在 onActivityResult
中,您将在数据中得到 Uri,它应该是 content://com.android.externalstorage.documents/tree/primary%3AAndroid
。进行必要的检查以验证用户是否确认了正确的文件夹。然后调用contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION)
保存权限,就可以了。
所以我们回到 ContentResolver.getPersistedUriPermissions()
,其中包含已授予权限的列表(可能还有更多),您在上面授予的权限如下所示:UriPermission {uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid, modeFlags=3}
(相同你在 onActivityResult
) 中得到的 Uri。从 getPersistedUriPermissions
迭代列表以找到感兴趣的 uri,如果找到则使用它,否则请求用户授予。
现在您想使用此“树”uri 和 Android 文件夹内文件的相对路径来处理 ContentResolver
和 DocumentsContract
。以下是列出 data
文件夹的示例:
data/
是相对于授予的“树”uri 的路径。使用 DocumentsContract.buildChildDocumentsUriUsingTree()
(列出文件)或 DocumentsContract.buildDocumentUriUsingTree()
(处理单个文件)构建最终的 uri,例如:DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri), DocumentsContract.getTreeDocumentId(treeUri)+"/data/")
,你会得到 uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid/document/primary%3AAndroid%2Fdata%2F/children
适合列出数据文件夹中的文件。现在调用 ContentResolver.query(uri, ...)
并处理 Cursor
中的数据以获取文件夹列表。
您使用其他 SAF 功能的方式类似于您可能已经知道的 read/write/rename/move/delete/create,使用 ContentResolver
或 DocumentsContract
的方法。
一些细节:
- 不需要
android.permission.MANAGE_EXTERNAL_STORAGE
- 它适用于目标 API 29 或 30
- 它仅适用于主存储,不适用于外部 SD 卡
- 对于数据文件夹中的所有文件,您需要使用 SAF(java 文件不起作用),只需使用相对于 Android 文件夹的文件层次结构
- 将来 Google 可能会在他们的“安全”意图中修补这个漏洞,并且在某些安全更新后这可能无法工作
编辑:示例代码,基于 Cheticamp Github sample。该示例显示了“Android”文件夹的每个子文件夹的内容(和文件数):
class MainActivity : AppCompatActivity() {
private val handleIntentActivityResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode != Activity.RESULT_OK)
return@registerForActivityResult
val directoryUri = it.data?.data ?: return@registerForActivityResult
contentResolver.takePersistableUriPermission(
directoryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
if (checkIfGotAccess())
onGotAccess()
else
Log.d("AppLog", "you didn't grant permission to the correct folder")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
val openDirectoryButton = findViewById<FloatingActionButton>(R.id.fab_open_directory)
openDirectoryButton.setOnClickListener {
openDirectory()
}
}
private fun checkIfGotAccess(): Boolean {
return contentResolver.persistedUriPermissions.indexOfFirst { uriPermission ->
uriPermission.uri.equals(androidTreeUri) && uriPermission.isReadPermission && uriPermission.isWritePermission
} >= 0
}
private fun onGotAccess() {
Log.d("AppLog", "got access to Android folder. showing content of each folder:")
@Suppress("DEPRECATION")
File(Environment.getExternalStorageDirectory(), "Android").listFiles()?.forEach { androidSubFolder ->
val docId = "$ANDROID_DOCID/${androidSubFolder.name}"
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(androidTreeUri, docId)
val contentResolver = this.contentResolver
Log.d("AppLog", "content of:${androidSubFolder.absolutePath} :")
contentResolver.query(childrenUri, null, null, null)
?.use { cursor ->
val filesCount = cursor.count
Log.d("AppLog", "filesCount:$filesCount")
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val mimeIndex = cursor.getColumnIndex("mime_type")
while (cursor.moveToNext()) {
val displayName = cursor.getString(nameIndex)
val mimeType = cursor.getString(mimeIndex)
Log.d("AppLog", " $displayName isFolder?${mimeType == DocumentsContract.Document.MIME_TYPE_DIR}")
}
}
}
}
private fun openDirectory() {
if (checkIfGotAccess())
onGotAccess()
else {
val primaryStorageVolume = (getSystemService(STORAGE_SERVICE) as StorageManager).primaryStorageVolume
val intent =
primaryStorageVolume.createOpenDocumentTreeIntent().putExtra(EXTRA_INITIAL_URI, androidUri)
handleIntentActivityResult.launch(intent)
}
}
companion object {
private const val ANDROID_DOCID = "primary:Android"
private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
private val androidUri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
private val androidTreeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
}
}
"Java 版本测试 Android 11" 这会将文件从 assets 文件夹复制到 android/data/xxx
中的任何目录@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class MainActivity extends AppCompatActivity
{
private final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents";
private final String ANDROID_DOCID =
"primary:Android/data/xxxxFolderName";
Uri uri;
Uri treeUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button b=findViewById(R.id.ok);
uri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
ANDROID_DOCID
);
treeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
ANDROID_DOCID
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
openDirectory();
}
}
private Boolean checkIfGotAccess() {
List<UriPermission> permissionList = getContentResolver().getPersistedUriPermissions();
for (int i = 0; i < permissionList.size(); i++) {
UriPermission it = permissionList.get(i);
if (it.getUri().equals(treeUri) && it.isReadPermission())
return true;
}
return false;
}
@RequiresApi(api = Build.VERSION_CODES.Q)
private void openDirectory() {
if (checkIfGotAccess()) {
copyFile(treeUri);
//return;
}
Intent intent =
getPrimaryVolume().createOpenDocumentTreeIntent()
.putExtra(EXTRA_INITIAL_URI, uri);
ActivityResultLauncher<Intent> handleIntentActivityResult = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
if (result.getData() == null || result.getData().getData() == null)
return;
Uri directoryUri = result.getData().getData();
getContentResolver().takePersistableUriPermission(
directoryUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
);
if (checkIfGotAccess())
copyFile(treeUri);
else
Log.d("AppLog", "you didn't grant permission to the correct folder");
}
});
handleIntentActivityResult.launch(intent);
}
@RequiresApi(api = Build.VERSION_CODES.N)
private StorageVolume getPrimaryVolume() {
StorageManager sm = (StorageManager) getSystemService(STORAGE_SERVICE);
return sm.getPrimaryStorageVolume();
}
private void copyFile(Uri treeUri) {
AssetManager assetManager = getAssets();
OutputStream out;
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
String extension = "ini";
try {
InputStream inn = assetManager.open("xxxxfileName.ini");
assert pickedDir != null;
DocumentFile existing = pickedDir.findFile("xxxxfileName.ini");
if(existing!=null)
existing.delete();
DocumentFile newFile = pickedDir.createFile("*/" + extension, "EnjoyCJZC.ini");
assert newFile != null;
out = getContentResolver().openOutputStream(newFile.getUri());
byte[] buffer = new byte[1024];
int read;
while ((read = inn.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
inn.close();
out.flush();
out.close();
} catch (Exception fnfe1) {
fnfe1.printStackTrace();
}
}
}