diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index de15c6dbd0..4312c28ecb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -50,6 +50,16 @@ SPDX-License-Identifier: GPL-3.0-or-later + + + + = Build.VERSION_CODES.N) { + FileProvider.getUriForFile(this, "${applicationInfo.packageName}.fileprovider", file) + } else { + file.toUri() + } + +fun Context.getFileFromUri(uri: Uri): File? { + Timber.d(uri.toString()) + val authority = uri.authority + val scheme = uri.scheme + val path = uri.path + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && path != null) { + val externals = arrayOf("/external/", "/external_path/") + for (external in externals) { + if (path.startsWith(external)) { + val f = + File( + Environment.getExternalStorageDirectory().absolutePath + + path.replace(external, "/"), + ) + if (f.exists()) { + Timber.d("$uri -> $external") + return f + } + } + } + val file = + if (path.startsWith("/files_path/")) { + File(filesDir.absolutePath + path.replace("/files_path/", "/")) + } else if (path.startsWith("/cache_path/")) { + File(cacheDir.absolutePath + path.replace("/cache_path/", "/")) + } else if (path.startsWith("/external_files_path/")) { + File(getExternalFilesDir(null)!!.absolutePath + path.replace("/external_files_path/", "/")) + } else if (path.startsWith("/external_cache_path/")) { + File(externalCacheDir!!.absolutePath + path.replace("/external_cache_path/", "/")) + } else { + null + } + if (file != null && file.exists()) { + Timber.d("$uri -> $path") + return file + } + } + return if (ContentResolver.SCHEME_FILE == scheme) { + if (path != null) return File(path) + Timber.d("$uri parse failed. -> 0") + null // end 0 + } else if (DocumentsContract.isDocumentUri(this, uri)) { + if ("com.android.externalstorage.documents" == authority) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":") + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + return File(Environment.getExternalStorageDirectory().toString() + "/" + split[1]) + } else { + // Below logic is how External Storage provider build URI for documents + // http://stackoverflow.com/questions/28605278/android-5-sd-card-label + val mStorageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager + runCatching { + val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume") + val getVolumeList = mStorageManager.javaClass.getMethod("getVolumeList") + val getUuid = storageVolumeClazz.getMethod("getUuid") + val getState = storageVolumeClazz.getMethod("getState") + val getPath = storageVolumeClazz.getMethod("getPath") + val isPrimary = storageVolumeClazz.getMethod("isPrimary") + val isEmulated = storageVolumeClazz.getMethod("isEmulated") + val result = getVolumeList.invoke(mStorageManager) + val length = result?.let { Array.getLength(it) } ?: 0 + for (i in 0 until length) { + val storageVolumeElement = Array.get(result, i) + val mounted = + Environment.MEDIA_MOUNTED == + getState.invoke( + storageVolumeElement, + ) || Environment.MEDIA_MOUNTED_READ_ONLY == getState.invoke(storageVolumeElement) + + // if the media is not mounted, we need not get the volume details + if (!mounted) continue + + // Primary storage is already handled. + if (isPrimary.invoke(storageVolumeElement) as Boolean && + isEmulated.invoke(storageVolumeElement) as Boolean + ) { + continue + } + val uuid = getUuid.invoke(storageVolumeElement) as? String + if (uuid != null && uuid == type) { + return File(getPath.invoke(storageVolumeElement).toString() + "/" + split[1]) + } + } + }.getOrElse { + Timber.d("$uri parse failed. $it -> 1_0") + } + } + Timber.d("$uri parse failed. -> 1_0") + null // end 1_0 + } else if ("com.android.providers.downloads.documents" == authority) { + var id = DocumentsContract.getDocumentId(uri) + if (id.isEmpty()) { + Timber.d("$uri parse failed(id is null). -> 1_1") + return null + } + if (id.startsWith("raw:")) { + return File(id.substring(4)) + } else if (id.startsWith("msf:")) { + id = id.split(":")[1] + } + val availableId: Long = + try { + id.toLong() + } catch (e: Exception) { + return null + } + val contentUriPrefixesToTry = + arrayOf( + "content://downloads/public_downloads", + "content://downloads/all_downloads", + "content://downloads/my_downloads", + ) + for (contentUriPrefix in contentUriPrefixesToTry) { + val contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), availableId) + try { + val file = getFileFromUri(contentUri, "1_1") + if (file != null) { + return file + } + } catch (ignore: Exception) { + } + } + Timber.d("$uri parse failed. -> 1_1") + null // end 1_1 + } else if ("com.android.providers.media.documents" == authority) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":") + val type = split[0] + val contentUri: Uri = + when (type) { + "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + else -> { + Timber.d("$uri parse failed. -> 1_2") + return null + } + } + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + getFileFromUri(contentUri, "1_2", selection, selectionArgs) // end 1_2 + } else if (ContentResolver.SCHEME_CONTENT == scheme) { + getFileFromUri(uri, code = "1_3") // end 1_3 + } else { + Timber.d("$uri parse failed. -> 1_4") + null // end 1_4 + } // end 1 + } else if (ContentResolver.SCHEME_CONTENT == scheme) { + getFileFromUri(uri, "2") // end 2 + } else { + Timber.d("$uri parse failed. -> 3") + null + } // end 3 +} + +private fun Context.getFileFromUri( + uri: Uri, + code: String, + selection: String? = null, + selectionArgs: kotlin.Array? = null, +): File? { + when (uri.authority) { + "com.google.android.apps.photos.content" -> { + val path = uri.lastPathSegment + if (!path.isNullOrEmpty()) { + return File(path) + } + } + "com.tencent.mtt.fileprovider" -> { + val path = uri.path + if (!path.isNullOrEmpty()) { + val fileDir = Environment.getExternalStorageDirectory() + return File(fileDir, path.substring("/QQBrowser".length, path.length)) + } + } + "com.huawei.hidisk.fileprovider" -> { + val path = uri.path + if (!path.isNullOrEmpty()) { + return File(path.replace("/root", "")) + } + } + } + + contentResolver.query(uri, arrayOf("_data"), selection, selectionArgs, null).use { + if (it == null) { + Timber.d("$uri parse failed(cursor is null). -> $code") + return null + } + return runCatching { + if (it.moveToFirst()) { + val columnIndex = it.getColumnIndex("_data") + if (columnIndex > -1) { + File(it.getString(columnIndex)) + } else { + Timber.d("$uri parse failed(columnIndex: $columnIndex is wrong). -> $code") + null + } + } else { + Timber.d("$uri parse failed(moveToFirst return false). -> $code") + null + } + }.getOrElse { + Timber.d("$uri parse failed. -> $code") + null + } + } +} diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000000..b2af9c8958 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + +