I tried Claude and here is the answer
Taifun
I’ll provide a comprehensive guide on using Storage Access Framework (SAF) to access /Android/data/my.target.apk
. Here’s a complete implementation:
// MainActivity.java
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.documentfile.provider.DocumentFile;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "SAFAccess";
private static final String PREF_NAME = "saf_prefs";
private static final String PREF_URI_KEY = "android_data_uri";
private static final String TARGET_PACKAGE = "my.target.apk";
private SharedPreferences prefs;
private Uri grantedUri;
// Activity result launcher for directory picker
private final ActivityResultLauncher<Intent> directoryPickerLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
if (uri != null) {
handleDirectorySelection(uri);
}
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE);
Button requestAccessBtn = findViewById(R.id.btnRequestAccess);
Button listFilesBtn = findViewById(R.id.btnListFiles);
Button readFileBtn = findViewById(R.id.btnReadFile);
Button writeFileBtn = findViewById(R.id.btnWriteFile);
requestAccessBtn.setOnClickListener(v -> requestAndroidDataAccess());
listFilesBtn.setOnClickListener(v -> listTargetAppFiles());
readFileBtn.setOnClickListener(v -> readSampleFile());
writeFileBtn.setOnClickListener(v -> writeSampleFile());
// Load previously granted URI
loadSavedUri();
}
/**
* Request access to Android/data directory using SAF
*/
private void requestAndroidDataAccess() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
// Try to open directly to Android/data if possible
try {
Uri androidDataUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata");
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidDataUri);
} catch (Exception e) {
Log.w(TAG, "Could not set initial URI", e);
}
// Add flags to persist permissions
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
directoryPickerLauncher.launch(intent);
}
/**
* Handle the selected directory URI
*/
private void handleDirectorySelection(Uri uri) {
try {
// Take persistent permissions
getContentResolver().takePersistableUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Save the URI for future use
prefs.edit().putString(PREF_URI_KEY, uri.toString()).apply();
grantedUri = uri;
// Verify this is the Android/data directory
DocumentFile docFile = DocumentFile.fromTreeUri(this, uri);
if (docFile != null && docFile.getName() != null) {
Log.d(TAG, "Granted access to: " + docFile.getName());
Toast.makeText(this, "Access granted to: " + docFile.getName(), Toast.LENGTH_SHORT).show();
// Check if target app directory exists
checkTargetAppDirectory();
}
} catch (SecurityException e) {
Log.e(TAG, "Failed to take persistent permission", e);
Toast.makeText(this, "Failed to grant persistent access", Toast.LENGTH_SHORT).show();
}
}
/**
* Load previously saved URI
*/
private void loadSavedUri() {
String uriString = prefs.getString(PREF_URI_KEY, null);
if (uriString != null) {
grantedUri = Uri.parse(uriString);
Log.d(TAG, "Loaded saved URI: " + uriString);
}
}
/**
* Check if target app directory exists
*/
private void checkTargetAppDirectory() {
if (grantedUri == null) {
Toast.makeText(this, "No access granted", Toast.LENGTH_SHORT).show();
return;
}
DocumentFile androidDataDir = DocumentFile.fromTreeUri(this, grantedUri);
if (androidDataDir != null) {
DocumentFile targetAppDir = androidDataDir.findFile(TARGET_PACKAGE);
if (targetAppDir != null && targetAppDir.isDirectory()) {
Log.d(TAG, "Target app directory found: " + TARGET_PACKAGE);
Toast.makeText(this, "Target app directory found!", Toast.LENGTH_SHORT).show();
} else {
Log.w(TAG, "Target app directory not found: " + TARGET_PACKAGE);
Toast.makeText(this, "Target app directory not found", Toast.LENGTH_SHORT).show();
}
}
}
/**
* List files in the target app's directory
*/
private void listTargetAppFiles() {
if (grantedUri == null) {
Toast.makeText(this, "Please grant access first", Toast.LENGTH_SHORT).show();
return;
}
try {
DocumentFile androidDataDir = DocumentFile.fromTreeUri(this, grantedUri);
if (androidDataDir == null) return;
DocumentFile targetAppDir = androidDataDir.findFile(TARGET_PACKAGE);
if (targetAppDir == null || !targetAppDir.isDirectory()) {
Toast.makeText(this, "Target app directory not found", Toast.LENGTH_SHORT).show();
return;
}
Log.d(TAG, "Listing files in: " + TARGET_PACKAGE);
listDirectoryContents(targetAppDir, 0);
} catch (Exception e) {
Log.e(TAG, "Error listing files", e);
Toast.makeText(this, "Error listing files: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* Recursively list directory contents
*/
private void listDirectoryContents(DocumentFile dir, int depth) {
if (dir == null || !dir.isDirectory()) return;
String indent = " ".repeat(depth);
DocumentFile[] files = dir.listFiles();
for (DocumentFile file : files) {
if (file.isDirectory()) {
Log.d(TAG, indent + "[DIR] " + file.getName());
// Recursively list subdirectories (be careful with depth)
if (depth < 3) {
listDirectoryContents(file, depth + 1);
}
} else {
Log.d(TAG, indent + "[FILE] " + file.getName() + " (" + file.length() + " bytes)");
}
}
}
/**
* Read a sample file from the target app directory
*/
private void readSampleFile() {
if (grantedUri == null) {
Toast.makeText(this, "Please grant access first", Toast.LENGTH_SHORT).show();
return;
}
try {
DocumentFile androidDataDir = DocumentFile.fromTreeUri(this, grantedUri);
if (androidDataDir == null) return;
DocumentFile targetAppDir = androidDataDir.findFile(TARGET_PACKAGE);
if (targetAppDir == null) {
Toast.makeText(this, "Target app directory not found", Toast.LENGTH_SHORT).show();
return;
}
// Look for a specific file (e.g., shared_prefs or files subdirectory)
DocumentFile filesDir = targetAppDir.findFile("files");
if (filesDir != null && filesDir.isDirectory()) {
DocumentFile[] files = filesDir.listFiles();
for (DocumentFile file : files) {
if (file.isFile() && file.canRead()) {
readFileContent(file);
break; // Read first readable file
}
}
}
} catch (Exception e) {
Log.e(TAG, "Error reading file", e);
Toast.makeText(this, "Error reading file: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* Read content from a DocumentFile
*/
private void readFileContent(DocumentFile file) {
try (InputStream inputStream = getContentResolver().openInputStream(file.getUri());
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
StringBuilder content = new StringBuilder();
String line;
int lineCount = 0;
while ((line = reader.readLine()) != null && lineCount < 10) { // Limit to first 10 lines
content.append(line).append("\n");
lineCount++;
}
Log.d(TAG, "File content from " + file.getName() + ":\n" + content.toString());
Toast.makeText(this, "Read " + lineCount + " lines from " + file.getName(), Toast.LENGTH_SHORT).show();
} catch (IOException e) {
Log.e(TAG, "Error reading file content", e);
}
}
/**
* Write a sample file to the target app directory (if writable)
*/
private void writeSampleFile() {
if (grantedUri == null) {
Toast.makeText(this, "Please grant access first", Toast.LENGTH_SHORT).show();
return;
}
try {
DocumentFile androidDataDir = DocumentFile.fromTreeUri(this, grantedUri);
if (androidDataDir == null) return;
DocumentFile targetAppDir = androidDataDir.findFile(TARGET_PACKAGE);
if (targetAppDir == null) {
Toast.makeText(this, "Target app directory not found", Toast.LENGTH_SHORT).show();
return;
}
// Try to create a file in the files subdirectory
DocumentFile filesDir = targetAppDir.findFile("files");
if (filesDir == null) {
filesDir = targetAppDir.createDirectory("files");
}
if (filesDir != null && filesDir.canWrite()) {
DocumentFile newFile = filesDir.createFile("text/plain", "saf_test.txt");
if (newFile != null) {
writeToFile(newFile, "Hello from SAF!\nTimestamp: " + System.currentTimeMillis());
}
}
} catch (Exception e) {
Log.e(TAG, "Error writing file", e);
Toast.makeText(this, "Error writing file: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* Write content to a DocumentFile
*/
private void writeToFile(DocumentFile file, String content) {
try (OutputStream outputStream = getContentResolver().openOutputStream(file.getUri())) {
if (outputStream != null) {
outputStream.write(content.getBytes());
Log.d(TAG, "Successfully wrote to: " + file.getName());
Toast.makeText(this, "File written: " + file.getName(), Toast.LENGTH_SHORT).show();
}
} catch (IOException e) {
Log.e(TAG, "Error writing to file", e);
}
}
/**
* Helper method to navigate to a specific subdirectory
*/
private DocumentFile navigateToSubdirectory(DocumentFile parentDir, String... pathSegments) {
DocumentFile currentDir = parentDir;
for (String segment : pathSegments) {
if (currentDir == null) break;
currentDir = currentDir.findFile(segment);
if (currentDir == null || !currentDir.isDirectory()) {
return null;
}
}
return currentDir;
}
/**
* Get specific app subdirectories (cache, files, shared_prefs, etc.)
*/
private void accessAppSubdirectories() {
if (grantedUri == null) return;
DocumentFile androidDataDir = DocumentFile.fromTreeUri(this, grantedUri);
if (androidDataDir == null) return;
DocumentFile targetAppDir = androidDataDir.findFile(TARGET_PACKAGE);
if (targetAppDir == null) return;
// Access common subdirectories
String[] commonDirs = {"cache", "files", "shared_prefs", "databases", "code_cache"};
for (String dirName : commonDirs) {
DocumentFile subDir = targetAppDir.findFile(dirName);
if (subDir != null && subDir.isDirectory()) {
Log.d(TAG, "Found subdirectory: " + dirName);
// Process files in this subdirectory
DocumentFile[] files = subDir.listFiles();
Log.d(TAG, " Contains " + files.length + " items");
}
}
}
}
Now let me also provide the required layout file and manifest permissions:
// MainActivity.java
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.documentfile.provider.DocumentFile;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "SAFAccess";
private static final String PREF_NAME = "saf_prefs";
private static final String PREF_URI_KEY = "android_data_uri";
private static final String TARGET_PACKAGE = "my.target.apk";
private SharedPreferences prefs;
private Uri grantedUri;
// Activity result launcher for directory picker
private final ActivityResultLauncher<Intent> directoryPickerLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
if (uri != null) {
handleDirectorySelection(uri);
}
// activity_main.xml
/*
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/btnRequestAccess"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Request Android/data Access"
android:layout_marginBottom="16dp" />
<Button
android:id="@+id/btnListFiles"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="List Target App Files"
android:layout_marginBottom="16dp" />
<Button
android:id="@+id/btnReadFile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Read Sample File"
android:layout_marginBottom="16dp" />
<Button
android:id="@+id/btnWriteFile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Write Sample File"
android:layout_marginBottom="16dp" />
</LinearLayout>
*/
// AndroidManifest.xml additions
/*
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- For DocumentFile operations -->
<application
android:requestLegacyExternalStorage="true"
... >
<!-- Your activities here -->
</application>
*/
// build.gradle (Module: app) dependencies
/*
dependencies {
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.activity:activity:1.7.2'
// ... other dependencies
}
*/
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE);
Button requestAccessBtn = findViewById(R.id.btnRequestAccess);
Button listFilesBtn = findViewById(R.id.btnListFiles);
Button readFileBtn = findViewById(R.id.btnReadFile);
Button writeFileBtn = findViewById(R.id.btnWriteFile);
requestAccessBtn.setOnClickListener(v -> requestAndroidDataAccess());
listFilesBtn.setOnClickListener(v -> listTargetAppFiles());
readFileBtn.setOnClickListener(v -> readSampleFile());
writeFileBtn.setOnClickListener(v -> writeSampleFile());
// Load previously granted URI
loadSavedUri();
}
/**
* Request access to Android/data directory using SAF
*/
private void requestAndroidDataAccess() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
// Try to open directly to Android/data if possible
try {
Uri androidDataUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata");
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidDataUri);
} catch (Exception e) {
Log.w(TAG, "Could not set initial URI", e);
}
// Add flags to persist permissions
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
directoryPickerLauncher.launch(intent);
}
/**
* Handle the selected directory URI
*/
private void handleDirectorySelection(Uri uri) {
try {
// Take persistent permissions
getContentResolver().takePersistableUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Save the URI for future use
prefs.edit().putString(PREF_URI_KEY, uri.toString()).apply();
grantedUri = uri;
// Verify this is the Android/data directory
DocumentFile docFile = DocumentFile.fromTreeUri(this, uri);
if (docFile != null && docFile.getName() != null) {
Log.d(TAG, "Granted access to: " + docFile.getName());
Toast.makeText(this, "Access granted to: " + docFile.getName(), Toast.LENGTH_SHORT).show();
// Check if target app directory exists
checkTargetAppDirectory();
}
} catch (SecurityException e) {
Log.e(TAG, "Failed to take persistent permission", e);
Toast.makeText(this, "Failed to grant persistent access", Toast.LENGTH_SHORT).show();
}
}
/**
* Load previously saved URI
*/
private void loadSavedUri() {
String uriString = prefs.getString(PREF_URI_KEY, null);
if (uriString != null) {
grantedUri = Uri.parse(uriString);
Log.d(TAG, "Loaded saved URI: " + uriString);
}
}
/**
* Check if target app directory exists
*/
private void checkTargetAppDirectory() {
if (grantedUri == null) {
Toast.makeText(this, "No access granted", Toast.LENGTH_SHORT).show();
return;
}
DocumentFile androidDataDir = DocumentFile.fromTreeUri(this, grantedUri);
if (androidDataDir != null) {
DocumentFile targetAppDir = androidDataDir.findFile(TARGET_PACKAGE);
if (targetAppDir != null && targetAppDir.isDirectory()) {
Log.d(TAG, "Target app directory found: " + TARGET_PACKAGE);
Toast.makeText(this, "Target app directory found!", Toast.LENGTH_SHORT).show();
} else {
Log.w(TAG, "Target app directory not found: " + TARGET_PACKAGE);
Toast.makeText(this, "Target app directory not found", Toast.LENGTH_SHORT).show();
}
}
}
/**
* List files in the target app's directory
*/
private void listTargetAppFiles() {
if (grantedUri == null) {
Toast.makeText(this, "Please grant access first", Toast.LENGTH_SHORT).show();
return;
}
try {
DocumentFile androidDataDir = DocumentFile.fromTreeUri(this, grantedUri);
if (androidDataDir == null) return;
DocumentFile targetAppDir = androidDataDir.findFile(TARGET_PACKAGE);
if (targetAppDir == null || !targetAppDir.isDirectory()) {
Toast.makeText(this, "Target app directory not found", Toast.LENGTH_SHORT).show();
return;
}
Log.d(TAG, "Listing files in: " + TARGET_PACKAGE);
listDirectoryContents(targetAppDir, 0);
} catch (Exception e) {
Log.e(TAG, "Error listing files", e);
Toast.makeText(this, "Error listing files: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* Recursively list directory contents
*/
private void listDirectoryContents(DocumentFile dir, int depth) {
if (dir == null || !dir.isDirectory()) return;
String indent = " ".repeat(depth);
DocumentFile[] files = dir.listFiles();
for (DocumentFile file : files) {
if (file.isDirectory()) {
Log.d(TAG, indent + "[DIR] " + file.getName());
// Recursively list subdirectories (be careful with depth)
if (depth < 3) {
listDirectoryContents(file, depth + 1);
}
} else {
Log.d(TAG, indent + "[FILE] " + file.getName() + " (" + file.length() + " bytes)");
}
}
}
/**
* Read a sample file from the target app directory
*/
private void readSampleFile() {
if (grantedUri == null) {
Toast.makeText(this, "Please grant access first", Toast.LENGTH_SHORT).show();
return;
}
try {
DocumentFile androidDataDir = DocumentFile.fromTreeUri(this, grantedUri);
if (androidDataDir == null) return;
DocumentFile targetAppDir = androidDataDir.findFile(TARGET_PACKAGE);
if (targetAppDir == null) {
Toast.makeText(this, "Target app directory not found", Toast.LENGTH_SHORT).show();
return;
}
// Look for a specific file (e.g., shared_prefs or files subdirectory)
DocumentFile filesDir = targetAppDir.findFile("files");
if (filesDir != null && filesDir.isDirectory()) {
DocumentFile[] files = filesDir.listFiles();
for (DocumentFile file : files) {
if (file.isFile() && file.canRead()) {
readFileContent(file);
break; // Read first readable file
}
}
}
} catch (Exception e) {
Log.e(TAG, "Error reading file", e);
Toast.makeText(this, "Error reading file: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* Read content from a DocumentFile
*/
private void readFileContent(DocumentFile file) {
try (InputStream inputStream = getContentResolver().openInputStream(file.getUri());
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
StringBuilder content = new StringBuilder();
String line;
int lineCount = 0;
while ((line = reader.readLine()) != null && lineCount < 10) { // Limit to first 10 lines
content.append(line).append("\n");
lineCount++;
}
Log.d(TAG, "File content from " + file.getName() + ":\n" + content.toString());
Toast.makeText(this, "Read " + lineCount + " lines from " + file.getName(), Toast.LENGTH_SHORT).show();
} catch (IOException e) {
Log.e(TAG, "Error reading file content", e);
}
}
/**
* Write a sample file to the target app directory (if writable)
*/
private void writeSampleFile() {
if (grantedUri == null) {
Toast.makeText(this, "Please grant access first", Toast.LENGTH_SHORT).show();
return;
}
try {
DocumentFile androidDataDir = DocumentFile.fromTreeUri(this, grantedUri);
if (androidDataDir == null) return;
DocumentFile targetAppDir = androidDataDir.findFile(TARGET_PACKAGE);
if (targetAppDir == null) {
Toast.makeText(this, "Target app directory not found", Toast.LENGTH_SHORT).show();
return;
}
// Try to create a file in the files subdirectory
DocumentFile filesDir = targetAppDir.findFile("files");
if (filesDir == null) {
filesDir = targetAppDir.createDirectory("files");
}
if (filesDir != null && filesDir.canWrite()) {
DocumentFile newFile = filesDir.createFile("text/plain", "saf_test.txt");
if (newFile != null) {
writeToFile(newFile, "Hello from SAF!\nTimestamp: " + System.currentTimeMillis());
}
}
} catch (Exception e) {
Log.e(TAG, "Error writing file", e);
Toast.makeText(this, "Error writing file: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
/**
* Write content to a DocumentFile
*/
private void writeToFile(DocumentFile file, String content) {
try (OutputStream outputStream = getContentResolver().openOutputStream(file.getUri())) {
if (outputStream != null) {
outputStream.write(content.getBytes());
Log.d(TAG, "Successfully wrote to: " + file.getName());
Toast.makeText(this, "File written: " + file.getName(), Toast.LENGTH_SHORT).show();
}
} catch (IOException e) {
Log.e(TAG, "Error writing to file", e);
}
}
/**
* Helper method to navigate to a specific subdirectory
*/
private DocumentFile navigateToSubdirectory(DocumentFile parentDir, String... pathSegments) {
DocumentFile currentDir = parentDir;
for (String segment : pathSegments) {
if (currentDir == null) break;
currentDir = currentDir.findFile(segment);
if (currentDir == null || !currentDir.isDirectory()) {
return null;
}
}
return currentDir;
}
/**
* Get specific app subdirectories (cache, files, shared_prefs, etc.)
*/
private void accessAppSubdirectories() {
if (grantedUri == null) return;
DocumentFile androidDataDir = DocumentFile.fromTreeUri(this, grantedUri);
if (androidDataDir == null) return;
DocumentFile targetAppDir = androidDataDir.findFile(TARGET_PACKAGE);
if (targetAppDir == null) return;
// Access common subdirectories
String[] commonDirs = {"cache", "files", "shared_prefs", "databases", "code_cache"};
for (String dirName : commonDirs) {
DocumentFile subDir = targetAppDir.findFile(dirName);
if (subDir != null && subDir.isDirectory()) {
Log.d(TAG, "Found subdirectory: " + dirName);
// Process files in this subdirectory
DocumentFile[] files = subDir.listFiles();
Log.d(TAG, " Contains " + files.length + " items");
}
}
}
}
Key SAF Implementation Details
1. Permission Flow:
- Request tree access via
ACTION_OPEN_DOCUMENT_TREE
- User selects
/Android/data
directory in the system file picker
- Take persistent permissions to avoid re-requesting
2. Navigation:
// Access target app: /Android/data/my.target.apk/
DocumentFile targetDir = androidDataDir.findFile("my.target.apk");
// Access subdirectories: files, cache, shared_prefs, etc.
DocumentFile filesDir = targetDir.findFile("files");
3. Important Considerations:
- User Interaction Required: SAF always requires user to manually select the directory
- Persistent Permissions: Use
takePersistableUriPermission()
to avoid re-requesting
- Limited Write Access: Many app directories are read-only
- Performance: SAF operations are slower than direct file access
4. Common Issues:
- Directory Not Found: Target app might not exist or have created the directory yet
- Permission Denied: Some subdirectories may be protected even with SAF access
- URI Changes: System updates can invalidate saved URIs
5. Best Practices:
- Always check if directories exist before accessing
- Handle SecurityException gracefully
- Cache DocumentFile references when possible
- Test on different Android versions and devices
This implementation provides a complete solution for accessing /Android/data/my.target.apk
using SAF, with proper error handling and user feedback. The user will need to manually grant access through the system file picker, but once granted, your app can persistently access the target directory.