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
相关产品推荐
相关产品推荐

