You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 08:06:00