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

Android 30及以上如何通过SAF将logcat日志保存到文件并分享

Android 11+ SAF适配logcat保存与分享方案

核心逻辑

SAF不需要申请外部存储权限,通过系统弹窗让用户主动选择日志保存目录,拿到授权URI后即可读写对应目录下的文件,完全符合谷歌存储规范。

分步实现

1. 启动SAF目录选择器

引导用户选择日志保存的根目录(一般建议选择公共Documents目录):

private static final int REQUEST_CODE_SELECT_DIR = 1001;

public void openLogDirChooser() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    // 可选:默认打开公共Documents目录
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.DIRECTORY_DOCUMENTS);
    startActivityForResult(intent, REQUEST_CODE_SELECT_DIR);
}

2. 处理用户选择的目录并持久化权限

用户选择目录后拿到URI,申请持久化权限,避免下次启动APP需要重新授权:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_SELECT_DIR && resultCode == RESULT_OK && data != null) {
        Uri selectedDirUri = data.getData();
        // 持久化读写权限
        getContentResolver().takePersistableUriPermission(selectedDirUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        // 执行日志写入逻辑
        saveLogcatWithSAF(selectedDirUri);
    }
}

3. 替换原有日志写入逻辑

通过SAF的URI创建目录、日志文件,将logcat输出写入到目标文件:

private void saveLogcatWithSAF(Uri rootDirUri) {
    ContentResolver resolver = getContentResolver();
    // 创建c4cex根目录
    Uri c4cDirUri = DocumentsContract.createDocument(resolver, rootDirUri,
            DocumentsContract.Document.MIME_TYPE_DIR, "c4cex");
    if (c4cDirUri == null) {
        // 目录已存在时查询获取已有URI
        c4cDirUri = findChildUri(rootDirUri, "c4cex", true);
    }
    if (c4cDirUri == null) return;

    // 创建logs子目录
    Uri logsDirUri = DocumentsContract.createDocument(resolver, c4cDirUri,
            DocumentsContract.Document.MIME_TYPE_DIR, "logs");
    if (logsDirUri == null) {
        logsDirUri = findChildUri(c4cDirUri, "logs", true);
    }
    if (logsDirUri == null) return;

    // 创建日志文件
    Uri logFileUri = DocumentsContract.createDocument(resolver, logsDirUri,
            "text/plain", "c4cex.txt");
    if (logFileUri == null) {
        // 覆盖已存在的旧日志
        Uri oldLogUri = findChildUri(logsDirUri, "c4cex.txt", false);
        if (oldLogUri != null) {
            resolver.delete(oldLogUri, null, null);
        }
        logFileUri = DocumentsContract.createDocument(resolver, logsDirUri,
                "text/plain", "c4cex.txt");
    }
    if (logFileUri == null) return;

    // 执行logcat命令并写入文件
    try {
        Process logcatProcess = Runtime.getRuntime().exec("logcat -r 2048 -n 4");
        InputStream logInput = logcatProcess.getInputStream();
        OutputStream logOutput = resolver.openOutputStream(logFileUri, "wa");

        byte[] buffer = new byte[1024];
        int len;
        while ((len = logInput.read(buffer)) != -1) {
            logOutput.write(buffer, 0, len);
        }
        logOutput.flush();
        logOutput.close();
        logInput.close();

        // 写入完成后调用邮件分享
        shareLogToEmail(logFileUri);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// 辅助方法:查询父目录下指定名称的子文件/目录URI
private Uri findChildUri(Uri parentUri, String displayName, boolean isDir) {
    ContentResolver resolver = getContentResolver();
    Uri childUri = DocumentsContract.buildChildDocumentsUriUsingTree(parentUri,
            DocumentsContract.getDocumentId(parentUri));
    String[] projection = {DocumentsContract.Document.COLUMN_DOCUMENT_ID,
            DocumentsContract.Document.COLUMN_DISPLAY_NAME};
    try (Cursor cursor = resolver.query(childUri, projection, null, null, null)) {
        while (cursor.moveToNext()) {
            String name = cursor.getString(1);
            if (displayName.equals(name)) {
                return DocumentsContract.buildDocumentUriUsingTree(parentUri, cursor.getString(0));
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

4. 邮件分享日志实现

拿到日志文件URI后,给邮件应用临时授权即可直接分享:

private void shareLogToEmail(Uri logFileUri) {
    Intent emailIntent = new Intent(Intent.ACTION_SEND);
    emailIntent.setType("message/rfc822");
    emailIntent.putExtra(Intent.EXTRA_SUBJECT, "应用运行日志");
    // 挂载日志文件附件
    emailIntent.putExtra(Intent.EXTRA_STREAM, logFileUri);
    // 给邮件应用临时授予读取权限
    emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    startActivity(Intent.createChooser(emailIntent, "选择邮件客户端发送日志"));
}

兼容处理

可以通过版本判断做兼容,Android 10及以下版本保留原有requestLegacyExternalStorage逻辑,Android 11及以上版本走SAF逻辑即可。


内容的提问来源于stack exchange,提问作者prat

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 16:15:06