Android 14(API34)文件选择器重复请求目录权限解决方案
Android 34 实现同一目录单次授权保存文件
问题描述
在targetSdk=34的Android原生Java项目中,使用存储访问框架(SAF)选择目录保存.txt文件时,已授权过的目录每次保存仍会重复请求权限,需要实现同一目录仅需单次授权的功能。
解决方案思路
Android 13+中,通过SAF获取的目录权限默认是临时的,重启App后会失效。要实现持久授权,需要:
- 获取目录Uri的持久化权限
- 将授权的Uri持久存储(比如用SharedPreferences)
- 后续保存时优先读取已存储的Uri,无需再次启动文件选择器
具体实现步骤及修改代码
1. 添加持久化存储相关变量和方法
在CreateFileActivity中添加SharedPreferences相关变量,用于保存授权的目录Uri:
private SharedPreferences prefs; private static final String PREFS_NAME = "FileSavePrefs"; private static final String KEY_SAVED_DIR_URI = "saved_dir_uri";
在onCreate方法中初始化SharedPreferences:
prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
添加方法读取和保存Uri:
// 读取保存的目录Uri private Uri getSavedDirectoryUri() { String uriString = prefs.getString(KEY_SAVED_DIR_URI, null); return uriString != null ? Uri.parse(uriString) : null; } // 保存目录Uri并获取持久权限 private void saveDirectoryUri(Uri uri) { // 获取持久化权限 getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // 保存到SharedPreferences prefs.edit().putString(KEY_SAVED_DIR_URI, uri.toString()).apply(); }
2. 修改保存按钮的点击逻辑
点击保存时,先检查是否有已保存的目录Uri,有则直接使用,无则启动文件选择器:
btnSaveFile.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Uri savedUri = getSavedDirectoryUri(); if (savedUri != null) { // 直接使用已授权的目录保存文件 createAndSaveTextFile(savedUri, edtFileName.getText().toString(), edtFileDesc.getText().toString()); } else { // 没有保存的Uri,启动文件选择器 filePicker.pickDirectory(CreateFileActivity.this); } } });
3. 修改文件保存方法和Activity结果回调
拆分原createAndSaveTextFileOnSDCard方法,新增直接用Uri保存的方法,并在onActivityResult中保存授权的Uri:
// 直接使用已保存的Uri创建文件 private void createAndSaveTextFile(Uri directoryUri, String fileName, String fileDesc) { DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri); if (documentFile != null && documentFile.exists()) { createFileInDirectory(documentFile, fileName, fileDesc); } else { Toast.makeText(this, "已授权目录不存在", Toast.LENGTH_SHORT).show(); // 清除无效的Uri prefs.edit().remove(KEY_SAVED_DIR_URI).apply(); } } // 原保存逻辑提取为公共方法 private void createFileInDirectory(DocumentFile directory, String fileName, String fileDesc) { DocumentFile file = directory.createFile("text/plain", fileName + ".txt"); try (OutputStream outputStream = getContentResolver().openOutputStream(file.getUri())) { if (outputStream != null) { String content = fileDesc; outputStream.write(content.getBytes()); Log.e("##PATH", file.getUri().toString()); Toast.makeText(this, "File saved: " + file.getUri().toString(), Toast.LENGTH_SHORT).show(); String fileContent = readTextFile(file.getUri()); Log.e("##FILE_CONTENT", fileContent); showFilesInDirectory(directory.getUri()); } } catch (IOException e) { e.printStackTrace(); Toast.makeText(this, "Error saving file: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == PICK_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { Uri uri = data.getData(); if (uri != null) { // 保存授权的Uri saveDirectoryUri(uri); DocumentFile documentFile = DocumentFile.fromTreeUri(this, uri); if (documentFile != null && documentFile.exists()) { createFileInDirectory(documentFile, edtFileName.getText().toString(), edtFileDesc.getText().toString()); } } } }
完整修改后的代码
import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.documentfile.provider.DocumentFile; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import com.bjit.fliecreateshowread.model.FileInfo; import com.bjit.fliecreateshowread.adapter.FileInfoListAdapter; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; public class CreateFileActivity extends AppCompatActivity { private EditText edtFileName; private EditText edtFileDesc; private Button btnSaveFile; private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 1; private static final int PICK_DIRECTORY_REQUEST_CODE = 123; private FilePicker filePicker; private List<FileInfo> fileInfoList; private RecyclerView rvFileInformationList; private SharedPreferences prefs; private static final String PREFS_NAME = "FileSavePrefs"; private static final String KEY_SAVED_DIR_URI = "saved_dir_uri"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_create_file); fileInfoList = new ArrayList<>(); filePicker = new FilePicker(); prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); edtFileName = findViewById(R.id.edtFileName); edtFileDesc = findViewById(R.id.edtFileDesc); btnSaveFile = findViewById(R.id.btnSaveFile); rvFileInformationList = findViewById(R.id.rvFileInformationList); btnSaveFile.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Uri savedUri = getSavedDirectoryUri(); if (savedUri != null) { createAndSaveTextFile(savedUri, edtFileName.getText().toString(), edtFileDesc.getText().toString()); } else { filePicker.pickDirectory(CreateFileActivity.this); } } }); } @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission granted, you can proceed with file creation } else { Toast.makeText(this, "Permission Denied!", Toast.LENGTH_SHORT).show(); } } } private void createAndSaveTextFile(Uri directoryUri, String fileName, String fileDesc) { DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri); if (documentFile != null && documentFile.exists()) { createFileInDirectory(documentFile, fileName, fileDesc); } else { Toast.makeText(this, "已授权目录不存在", Toast.LENGTH_SHORT).show(); prefs.edit().remove(KEY_SAVED_DIR_URI).apply(); } } private void createFileInDirectory(DocumentFile directory, String fileName, String fileDesc) { DocumentFile file = directory.createFile("text/plain", fileName + ".txt"); try (OutputStream outputStream = getContentResolver().openOutputStream(file.getUri())) { if (outputStream != null) { String content = fileDesc; outputStream.write(content.getBytes()); Log.e("##PATH", file.getUri().toString()); Toast.makeText(this, "File saved: " + file.getUri().toString(), Toast.LENGTH_SHORT).show(); String fileContent = readTextFile(file.getUri()); Log.e("##FILE_CONTENT", fileContent); showFilesInDirectory(directory.getUri()); } } catch (IOException e) { e.printStackTrace(); Toast.makeText(this, "Error saving file: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } private String readTextFile(Uri fileUri) { StringBuilder content = new StringBuilder(); try (InputStream inputStream = getContentResolver().openInputStream(fileUri); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { String line; while ((line = reader.readLine()) != null) { content.append(line).append("\n"); } } catch (IOException e) { e.printStackTrace(); Toast.makeText(this, "Error reading file: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } return content.toString(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == PICK_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { Uri uri = data.getData(); if (uri != null) { saveDirectoryUri(uri); DocumentFile documentFile = DocumentFile.fromTreeUri(this, uri); if (documentFile != null && documentFile.exists()) { createFileInDirectory(documentFile, edtFileName.getText().toString(), edtFileDesc.getText().toString()); } } } } private void showFilesInDirectory(Uri directoryUri) { DocumentFile directory = DocumentFile.fromTreeUri(this, directoryUri); if (directory != null && directory.exists() && directory.isDirectory()) { DocumentFile[] files = directory.listFiles(); fileInfoList.clear(); if (files != null && files.length > 0) { for (DocumentFile file : files) { Log.e("##FILE_IN_DIRECTORY", "File Name: " + file.getName() + ", File URI: " + file.getUri()); FileInfo fileInfo = new FileInfo(file.getName(), file.getUri().toString()); fileInfoList.add(fileInfo); } rvFileInformationList.setLayoutManager(new LinearLayoutManager(this)); FileInfoListAdapter adapter = new FileInfoListAdapter((ArrayList<FileInfo>) fileInfoList, CreateFileActivity.this); rvFileInformationList.setAdapter(adapter); adapter.notifyDataSetChanged(); } else { Log.e("##FILE_IN_DIRECTORY", "No files in the directory"); } } else { Log.e("##FILE_IN_DIRECTORY", "Invalid directory or directory does not exist"); } } private Uri getSavedDirectoryUri() { String uriString = prefs.getString(KEY_SAVED_DIR_URI, null); return uriString != null ? Uri.parse(uriString) : null; } private void saveDirectoryUri(Uri uri) { getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); prefs.edit().putString(KEY_SAVED_DIR_URI, uri.toString()).apply(); } }
说明
- 通过
takePersistableUriPermission获取的权限会持久化,即使App重启也不会失效 - 保存的Uri存储在SharedPreferences中,下次打开App时可直接读取使用
- 如果检测到已保存的目录不存在,会清除无效的Uri,重新引导用户选择目录
内容的提问来源于stack exchange,提问作者S. M. Asif
相关产品推荐
相关产品推荐

