From 7ba3ea3185e48cc732c3f25d34db1eb001d7d9c7 Mon Sep 17 00:00:00 2001 From: laoyuyu Date: Mon, 13 Feb 2023 20:28:11 +0800 Subject: [PATCH] thread task --- .../java/com/arialyy/aria/util/FileUri.kt | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 PublicComponent/src/main/java/com/arialyy/aria/util/FileUri.kt diff --git a/PublicComponent/src/main/java/com/arialyy/aria/util/FileUri.kt b/PublicComponent/src/main/java/com/arialyy/aria/util/FileUri.kt new file mode 100644 index 00000000..5db34fdf --- /dev/null +++ b/PublicComponent/src/main/java/com/arialyy/aria/util/FileUri.kt @@ -0,0 +1,387 @@ +package com.arialyy.aria.util + +import android.annotation.SuppressLint +import android.content.* +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.* +import androidx.core.content.FileProvider +import com.arialyy.aria.core.DuaContext +import timber.log.Timber +import java.io.* + +/** + * # FileUri + * + * - Uri & Path Tool + * + * @author javakam + * @date 2020/8/24 11:24 + */ +object FileUri { + + //Android R + //---------------------------------------------------------------- + + /** + * `MANAGE_EXTERNAL_STORAGE` 权限检查 + * + * @return `true` Have permission + */ + fun isExternalStorageManager(): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) Environment.isExternalStorageManager() else false + + /** + * 跳转到 `MANAGE_EXTERNAL_STORAGE` 权限设置页面 + * + * @return `true` Has been set + */ + fun jumpManageAppAllFilesPermissionSetting( + context: Context, + isNewTask: Boolean = false, + ): Boolean { + if (isExternalStorageManager()) return true + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + intent.data = Uri.parse("package:${context.packageName}") + if (isNewTask) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } catch (e: Exception) { + Timber.e("jumpManageAppAllFilesPermissionSetting: $e") + } + } + return false + } + + //从 FilePath 中获取 Uri (Get Uri from FilePath) + //---------------------------------------------------------------- + + fun getUriByPath(path: String?): Uri? = + if (path.isNullOrBlank()) null else getUriByFile(File(path)) + + /** + * Return a content URI for a given file. + * + * @param file The file. + * @param isOriginal true content:// or file:// ; false file://xxx + * @return a content URI for a given file + */ + fun getUriByFile(file: File?, isOriginal: Boolean = false): Uri? { + return if (isOriginal) Uri.fromFile(file) + else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val authority = DuaContext.context.packageName + AUTHORITY + FileProvider.getUriForFile(DuaContext.context, authority, file ?: return null) + } else { + Uri.fromFile(file) + } + } + } + + fun getShareUri(path: String?): Uri? = + if (path.isNullOrBlank()) null else getUriByFile(File(path), isOriginal = false) + + /** + * @return content:// or file:// + */ + fun getShareUri(file: File?): Uri? = getUriByFile(file, isOriginal = false) + + fun getOriginalUri(path: String?): Uri? = + if (path.isNullOrBlank()) null else getUriByFile(File(path), isOriginal = true) + + /** + * @return file://xxx + */ + fun getOriginalUri(file: File?): Uri? = getUriByFile(file, isOriginal = true) + + //获取Uri对应的文件路径, Compatible with API 26 + //---------------------------------------------------------------- + + /** + * ### Get the file path through Uri + * + * - Need permission: RequiresPermission(permission.READ_EXTERNAL_STORAGE) + * + * - Modified from: https://github.com/coltoscosmin/FileUtils/blob/master/FileUtils.java + * + * @return file path + */ + fun getPathByUri(uri: Uri?): String? { + return uri?.use { + Timber.i( + "FileUri getPathByUri -> " + + "Uri: " + uri + + ", Authority: " + uri.authority + + ", Fragment: " + uri.fragment + + ", Port: " + uri.port + + ", Query: " + uri.query + + ", Scheme: " + uri.scheme + + ", Host: " + uri.host + + ", Segments: " + uri.pathSegments.toString() + ) + + // 以 file:// 开头的使用第三方应用打开 (open with third-party applications starting with file://) + if (ContentResolver.SCHEME_FILE.equals(uri.scheme, ignoreCase = true)) return getDataColumn( + uri + ) + + @SuppressLint("ObsoleteSdkInt") + val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + + // Before 4.4 , API 19 content:// 开头, 比如 content://media/external/images/media/123 + if (!isKitKat && ContentResolver.SCHEME_CONTENT.equals(uri.scheme, true)) { + if (isGooglePhotosUri(uri)) return uri.lastPathSegment + return getDataColumn(uri) + } + + val context = DuaContext.context + // After 4.4 , API 19 + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // LocalStorageProvider + if (isLocalStorageDocument(uri)) { + // The path is the id + return DocumentsContract.getDocumentId(uri); + } + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":").toTypedArray() + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + .toString() + File.separator + split[1] + } else { + @Suppress("DEPRECATION") + return Environment.getExternalStorageDirectory() + .toString() + File.separator + split[1] + } + } else if ("home".equals(type, ignoreCase = true)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + .toString() + File.separator + "documents" + File.separator + split[1] + } else { + @Suppress("DEPRECATION") + return Environment.getExternalStorageDirectory() + .toString() + File.separator + "documents" + File.separator + split[1] + } + } else { + @Suppress("DEPRECATION") + val sdcardPath = + Environment.getExternalStorageDirectory() + .toString() + File.separator + "documents" + File.separator + split[1] + return if (sdcardPath.startsWith("file://")) { + sdcardPath.replace("file://", "") + } else { + sdcardPath + } + } + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + val id = DocumentsContract.getDocumentId(uri) + if (id != null && id.startsWith("raw:")) { + return id.substring(4) + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + val contentUriPrefixesToTry = arrayOf( + "content://downloads/public_downloads", + "content://downloads/my_downloads", + "content://downloads/all_downloads" + ) + for (contentUriPrefix in contentUriPrefixesToTry) { + val contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), id.toLong()) + try { + val path = getDataColumn(contentUri) + if (!path.isNullOrBlank()) return path + } catch (e: Exception) { + Timber.e(e.toString()) + } + } + } else { + //testPath(uri) + return getDataColumn(uri) + } + } + // MediaProvider + else if (isMediaDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":").toTypedArray() + val contentUri: Uri? = when (split[0]) { + "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "download" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStore.Downloads.EXTERNAL_CONTENT_URI + } else null + else -> null + } + val selectionArgs = arrayOf(split[1]) + return getDataColumn(contentUri, "_id=?", selectionArgs) + } + + //GoogleDriveProvider + else if (isGoogleDriveUri(uri)) { + return getGoogleDriveFilePath(uri, context) + } + } + // MediaStore (and general) + else if ("content".equals(uri.scheme, ignoreCase = true)) { + // Return the remote address + if (isGooglePhotosUri(uri)) { + return uri.lastPathSegment + } + // Google drive legacy provider + else if (isGoogleDriveUri(uri)) { + return getGoogleDriveFilePath(uri, context) + } + // Huawei + else if (isHuaWeiUri(uri)) { + val uriPath = getDataColumn(uri) ?: uri.toString() + //content://com.huawei.hidisk.fileprovider/root/storage/emulated/0/Android/data/com.xxx.xxx/ + if (uriPath.startsWith("/root")) { + return uriPath.replace("/root".toRegex(), "") + } + } + return getDataColumn(uri) + } + return getDataColumn(uri) + } + } + + /** + * BUG : 部分机型进入"文件管理器" 执行到 cursor.getColumnIndexOrThrow(column);出现 + * Caused by: java.lang.IllegalArgumentException: column '_data' does not exist. Available columns: [] + * + * Fixed : + * https://stackoverflow.com/questions/42508383/illegalargumentexception-column-data-does-not-exist + * + */ + private fun getDataColumn( + uri: Uri?, + selection: String? = null, + selectionArgs: Array? = null + ): String? { + @Suppress("DEPRECATION") + val column = MediaStore.Files.FileColumns.DATA + val projection = arrayOf(column) + try { + DuaContext.context.contentResolver.query( + uri ?: return null, + projection, + selection, + selectionArgs, + null + )?.use { c: Cursor -> + if (c.moveToFirst()) { + val columnIndex = c.getColumnIndex(column) + return c.getString(columnIndex) + } + } + } catch (e: Throwable) { + Timber.e("getDataColumn -> ${e.message}") + } + return null + } + + //The Uri to check + //---------------------------------------------------------------- + + private fun getGoogleDriveFilePath(uri: Uri, context: Context): String? { + context.contentResolver.query(uri, null, null, null, null)?.use { c: Cursor -> + /* + Get the column indexes of the data in the Cursor, + move to the first row in the Cursor, get the data, and display it. + */ + val nameIndex: Int = c.getColumnIndex(OpenableColumns.DISPLAY_NAME) + //val sizeIndex: Int = c.getColumnIndex(OpenableColumns.SIZE) + if (!c.moveToFirst()) { + return uri.toString() + } + val name: String = c.getString(nameIndex) + //val size = c.getLong(sizeIndex).toString() + val file = File(context.cacheDir, name) + + var inputStream: InputStream? = null + var outputStream: FileOutputStream? = null + try { + inputStream = context.contentResolver.openInputStream(uri) + outputStream = FileOutputStream(file) + var read = 0 + val maxBufferSize = 1 * 1024 * 1024 + val bytesAvailable: Int = inputStream?.available() ?: 0 + val bufferSize = bytesAvailable.coerceAtMost(maxBufferSize) + val buffers = ByteArray(bufferSize) + while (inputStream?.read(buffers)?.also { read = it } != -1) { + outputStream.write(buffers, 0, read) + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + inputStream?.close() + outputStream?.close() + } + return file.path + } + return uri.toString() + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + fun isGooglePhotosUri(uri: Uri?): Boolean { + return "com.google.android.apps.photos.content".equals(uri?.authority, true) + } + + fun isGoogleDriveUri(uri: Uri?): Boolean { + return "com.google.android.apps.docs.storage.legacy" == uri?.authority || "com.google.android.apps.docs.storage" == uri?.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is local. + */ + fun isLocalStorageDocument(uri: Uri?): Boolean { + return AUTHORITY.equals(uri?.authority, true) + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private fun isExternalStorageDocument(uri: Uri?): Boolean { + return "com.android.externalstorage.documents".equals(uri?.authority, true) + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private fun isDownloadsDocument(uri: Uri?): Boolean { + return "com.android.providers.downloads.documents".equals(uri?.authority, true) + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private fun isMediaDocument(uri: Uri?): Boolean { + return "com.android.providers.media.documents".equals(uri?.authority, true) + } + + /** + * content://com.huawei.hidisk.fileprovider/root/storage/emulated/0/Android/data/com.xxx.xxx/ + * + * @param uri + * @return + */ + private fun isHuaWeiUri(uri: Uri?): Boolean { + return "com.huawei.hidisk.fileprovider".equals(uri?.authority, true) + } +} \ No newline at end of file