package com.dcx.ruiyiweiux; import android.app.Activity; import android.content.Context; 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.webkit.MimeTypeMap; import java.io.OutputStream; import java.nio.charset.StandardCharsets; public class UsbStorageAccess { private static final String TAG = "DCXUsbStorageAccess"; private static final String PREFS_NAME = "dcx_usb_storage_access"; private static final String PREF_TREE_URI = "tree_uri"; private static final int REQUEST_OPEN_TREE = 42001; public static boolean hasPersistedUsbDirectory(Activity activity) { String treeUri = getPersistedTreeUri(activity); return treeUri != null && treeUri.length() > 0; } public static String getPersistedUsbDirectoryUri(Activity activity) { String treeUri = getPersistedTreeUri(activity); return treeUri == null ? "" : treeUri; } public static void requestUsbDirectoryPermission(Activity activity) { Intent intent = new Intent(activity, ActivityBridge.class); activity.startActivity(intent); } public static void clearPersistedUsbDirectory(Activity activity) { String treeUriText = getPersistedTreeUri(activity); if (treeUriText != null && treeUriText.length() > 0) { try { Uri uri = Uri.parse(treeUriText); activity.getContentResolver().releasePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } catch (Exception ex) { Log.w(TAG, "release persisted tree uri failed", ex); } } activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) .edit() .remove(PREF_TREE_URI) .apply(); } public static boolean writeTextFile(Activity activity, String fileName, String content) { byte[] bytes = content == null ? new byte[0] : content.getBytes(StandardCharsets.UTF_8); return writeBytesFile(activity, fileName, bytes); } public static boolean writeBytesFile(Activity activity, String fileName, byte[] bytes) { try { String treeUriText = getPersistedTreeUri(activity); if (treeUriText == null || treeUriText.length() == 0) { return false; } Uri treeUri = Uri.parse(treeUriText); Uri exportDir = ensureDirectory(activity, treeUri, "DCX_Export"); if (exportDir == null) { return false; } Uri fileUri = createOrReplaceFile(activity, exportDir, fileName); if (fileUri == null) { return false; } OutputStream stream = activity.getContentResolver().openOutputStream(fileUri, "wt"); if (stream == null) { return false; } try { stream.write(bytes == null ? new byte[0] : bytes); stream.flush(); } finally { stream.close(); } return true; } catch (Exception ex) { Log.w(TAG, "write usb file failed: " + fileName, ex); return false; } } private static Uri ensureDirectory(Activity activity, Uri treeUri, String displayName) { try { String treeDocumentId = DocumentsContract.getTreeDocumentId(treeUri); if (isSelectedTreeDirectory(treeDocumentId, displayName)) { return DocumentsContract.buildDocumentUriUsingTree(treeUri, treeDocumentId); } Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( treeUri, treeDocumentId); android.database.Cursor cursor = activity.getContentResolver().query( childrenUri, new String[]{ DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE }, null, null, null); if (cursor != null) { try { while (cursor.moveToNext()) { String documentId = cursor.getString(0); String name = cursor.getString(1); String mimeType = cursor.getString(2); if (displayName.equals(name) && DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) { return DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId); } } } finally { cursor.close(); } } return DocumentsContract.createDocument( activity.getContentResolver(), DocumentsContract.buildDocumentUriUsingTree( treeUri, treeDocumentId), DocumentsContract.Document.MIME_TYPE_DIR, displayName); } catch (Exception ex) { Log.w(TAG, "ensure usb export directory failed", ex); return null; } } private static boolean isSelectedTreeDirectory(String treeDocumentId, String displayName) { if (treeDocumentId == null || displayName == null) { return false; } int colonIndex = treeDocumentId.indexOf(':'); String pathPart = colonIndex >= 0 ? treeDocumentId.substring(colonIndex + 1) : treeDocumentId; while (pathPart.endsWith("/") && pathPart.length() > 0) { pathPart = pathPart.substring(0, pathPart.length() - 1); } int slashIndex = Math.max(pathPart.lastIndexOf('/'), pathPart.lastIndexOf('\\')); String selectedName = slashIndex >= 0 ? pathPart.substring(slashIndex + 1) : pathPart; return displayName.equals(selectedName); } private static Uri createOrReplaceFile(Activity activity, Uri parentUri, String fileName) { try { String safeName = sanitizeFileName(fileName); String mimeType = getMimeType(safeName); Uri existingFile = findChild(activity, parentUri, safeName, null); if (existingFile != null) { DocumentsContract.deleteDocument(activity.getContentResolver(), existingFile); } return DocumentsContract.createDocument( activity.getContentResolver(), parentUri, mimeType, safeName); } catch (Exception ex) { Log.w(TAG, "create usb export file failed: " + fileName, ex); return null; } } private static Uri findChild(Activity activity, Uri parentUri, String displayName, String requiredMimeType) { android.database.Cursor cursor = null; try { Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( parentUri, DocumentsContract.getDocumentId(parentUri)); cursor = activity.getContentResolver().query( childrenUri, new String[]{ DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE }, null, null, null); if (cursor == null) { return null; } while (cursor.moveToNext()) { String documentId = cursor.getString(0); String name = cursor.getString(1); String mimeType = cursor.getString(2); if (displayName.equals(name) && (requiredMimeType == null || requiredMimeType.equals(mimeType))) { return DocumentsContract.buildDocumentUriUsingTree(parentUri, documentId); } } } catch (Exception ignored) { } finally { if (cursor != null) { cursor.close(); } } return null; } private static String getPersistedTreeUri(Context context) { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); return prefs.getString(PREF_TREE_URI, ""); } private static void savePersistedTreeUri(Context context, Uri uri) { context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) .edit() .putString(PREF_TREE_URI, uri.toString()) .apply(); } private static boolean isLikelyUsbTree(Uri uri) { try { String documentId = DocumentsContract.getTreeDocumentId(uri); if (documentId == null || documentId.length() == 0) { return false; } String lowerDocumentId = documentId.toLowerCase(); return !lowerDocumentId.startsWith("primary:") && !lowerDocumentId.startsWith("home:"); } catch (Exception ex) { Log.w(TAG, "check selected tree uri failed", ex); return false; } } private static String sanitizeFileName(String fileName) { if (fileName == null || fileName.trim().length() == 0) { return "BFI_Export.txt"; } return fileName.replaceAll("[\\\\/:*?\"<>|]", "_"); } private static String getMimeType(String fileName) { String extension = MimeTypeMap.getFileExtensionFromUrl(fileName); if (extension != null && extension.length() > 0) { String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase()); if (mimeType != null && mimeType.length() > 0) { return mimeType; } } if (fileName.endsWith(".csv")) { return "text/csv"; } if (fileName.endsWith(".pdf")) { return "application/pdf"; } return "text/plain"; } public static class ActivityBridge extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); startActivityForResult(intent, REQUEST_OPEN_TREE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_OPEN_TREE && resultCode == RESULT_OK && data != null) { Uri uri = data.getData(); if (uri != null) { if (!isLikelyUsbTree(uri)) { Log.w(TAG, "selected tree is internal storage, ignore: " + uri); finish(); return; } int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); boolean persisted = false; try { getContentResolver().takePersistableUriPermission( uri, flags & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)); persisted = true; } catch (Exception ex) { Log.w(TAG, "persist selected usb tree uri failed", ex); } if (persisted) { savePersistedTreeUri(this, uri); } } } finish(); } } }