mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-20 02:00:55 +07:00
fix: support filepicker android 9
This commit is contained in:
@@ -42,6 +42,7 @@ import androidx.core.view.OnApplyWindowInsetsListener
|
|||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.LazyThreadSafetyMode.NONE
|
import kotlin.LazyThreadSafetyMode.NONE
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
@@ -766,7 +767,13 @@ class AmneziaActivity : QtActivity() {
|
|||||||
fun openFile(filter: String?) {
|
fun openFile(filter: String?) {
|
||||||
Log.v(TAG, "Open file with filter: $filter")
|
Log.v(TAG, "Open file with filter: $filter")
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
val intent = if (!isOnTv()) {
|
val systemPickerPackage = listOf("com.google.android.documentsui", "com.android.documentsui")
|
||||||
|
.firstOrNull { pkg ->
|
||||||
|
try { packageManager.getPackageInfo(pkg, 0); true }
|
||||||
|
catch (_: PackageManager.NameNotFoundException) { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = if (!isOnTv() && systemPickerPackage != null) {
|
||||||
val mimeTypes = if (!filter.isNullOrEmpty()) {
|
val mimeTypes = if (!filter.isNullOrEmpty()) {
|
||||||
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
|
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
|
||||||
val mime = MimeTypeMap.getSingleton()
|
val mime = MimeTypeMap.getSingleton()
|
||||||
@@ -792,16 +799,7 @@ class AmneziaActivity : QtActivity() {
|
|||||||
else -> type = "*/*"
|
else -> type = "*/*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Force system document picker to avoid third-party file managers
|
`package` = systemPickerPackage
|
||||||
// that may lack storage permissions (common on Android TV devices)
|
|
||||||
val systemPickerPackage = listOf("com.google.android.documentsui", "com.android.documentsui")
|
|
||||||
.firstOrNull { pkg ->
|
|
||||||
try { packageManager.getPackageInfo(pkg, 0); true }
|
|
||||||
catch (_: PackageManager.NameNotFoundException) { false }
|
|
||||||
}
|
|
||||||
if (systemPickerPackage != null) {
|
|
||||||
`package` = systemPickerPackage
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Intent(this@AmneziaActivity, TvFilePicker::class.java)
|
Intent(this@AmneziaActivity, TvFilePicker::class.java)
|
||||||
@@ -813,8 +811,11 @@ class AmneziaActivity : QtActivity() {
|
|||||||
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
|
if (isOnTv() && it?.hasExtra("activityNotFound") == true) {
|
||||||
showNoFileBrowserAlertDialog()
|
showNoFileBrowserAlertDialog()
|
||||||
}
|
}
|
||||||
val uri = it?.data?.apply {
|
val uri = it?.data?.let { u ->
|
||||||
grantUriPermission(packageName, this, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
if (u.scheme == "content") {
|
||||||
|
try { grantUriPermission(packageName, u, Intent.FLAG_GRANT_READ_URI_PERMISSION) } catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
u
|
||||||
}?.toString() ?: ""
|
}?.toString() ?: ""
|
||||||
Log.v(TAG, "Open file: $uri")
|
Log.v(TAG, "Open file: $uri")
|
||||||
if (uri.isNotEmpty()) {
|
if (uri.isNotEmpty()) {
|
||||||
@@ -854,7 +855,12 @@ class AmneziaActivity : QtActivity() {
|
|||||||
Log.v(TAG, "Get fd for $fileName")
|
Log.v(TAG, "Get fd for $fileName")
|
||||||
return blockingCall(Dispatchers.IO) {
|
return blockingCall(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
|
val uri = Uri.parse(fileName)
|
||||||
|
pfd = if (uri.scheme == "file") {
|
||||||
|
ParcelFileDescriptor.open(File(uri.path!!), ParcelFileDescriptor.MODE_READ_ONLY)
|
||||||
|
} else {
|
||||||
|
contentResolver.openFileDescriptor(uri, "r")
|
||||||
|
}
|
||||||
pfd?.fd ?: -1
|
pfd?.fd ?: -1
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to get fd: $e")
|
Log.e(TAG, "Failed to get fd: $e")
|
||||||
|
|||||||
@@ -1,30 +1,36 @@
|
|||||||
package org.amnezia.vpn
|
package org.amnezia.vpn
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import org.amnezia.vpn.util.Log
|
import org.amnezia.vpn.util.Log
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
private const val TAG = "TvFilePicker"
|
private const val TAG = "TvFilePicker"
|
||||||
|
private const val READ_STORAGE_REQUEST_CODE = 1001
|
||||||
|
|
||||||
class TvFilePicker : ComponentActivity() {
|
class TvFilePicker : ComponentActivity() {
|
||||||
|
|
||||||
private val fileChooseResultLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
// SAF launcher for Android 10+ where File API is blocked by scoped storage
|
||||||
|
private val safLauncher = registerForActivityResult(object : ActivityResultContracts.OpenDocument() {
|
||||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||||
val intent = super.createIntent(context, input)
|
val intent = super.createIntent(context, input)
|
||||||
|
val activities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val activitiesToResolveIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
|
||||||
} else {
|
} else {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
}
|
}
|
||||||
if (activitiesToResolveIntent.all {
|
if (activities.all {
|
||||||
val name = it.activityInfo.packageName
|
val name = it.activityInfo.packageName
|
||||||
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
|
name.startsWith("com.google.android.tv.frameworkpackagestubs") || name.startsWith("com.android.tv.frameworkpackagestubs")
|
||||||
}) {
|
}) {
|
||||||
@@ -32,38 +38,140 @@ class TvFilePicker : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
}) {
|
}) { uri ->
|
||||||
setResult(RESULT_OK, Intent().apply {
|
setResult(RESULT_OK, Intent().apply {
|
||||||
data = it
|
data = uri
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
})
|
})
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val directoryStack = ArrayDeque<File>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.v(TAG, "onCreate")
|
Log.v(TAG, "onCreate")
|
||||||
getFile()
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
launchSaf()
|
||||||
|
} else {
|
||||||
|
checkPermissionAndBrowse()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
@Deprecated("Deprecated in Java")
|
||||||
super.onNewIntent(intent)
|
override fun onBackPressed() {
|
||||||
Log.v(TAG, "onNewIntent")
|
navigateBack()
|
||||||
getFile()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFile() {
|
private fun launchSaf() {
|
||||||
try {
|
try {
|
||||||
Log.v(TAG, "getFile")
|
safLauncher.launch(arrayOf("*/*"))
|
||||||
fileChooseResultLauncher.launch(arrayOf("*/*"))
|
|
||||||
} catch (_: ActivityNotFoundException) {
|
} catch (_: ActivityNotFoundException) {
|
||||||
Log.w(TAG, "Activity not found")
|
Log.w(TAG, "No SAF activity found")
|
||||||
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
setResult(RESULT_CANCELED, Intent().apply { putExtra("activityNotFound", true) })
|
||||||
finish()
|
finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to get file: $e")
|
Log.e(TAG, "SAF launch failed: $e")
|
||||||
setResult(RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkPermissionAndBrowse() {
|
||||||
|
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), READ_STORAGE_REQUEST_CODE)
|
||||||
|
} else {
|
||||||
|
showRootDirectory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == READ_STORAGE_REQUEST_CODE &&
|
||||||
|
grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
showRootDirectory()
|
||||||
|
} else {
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showRootDirectory() {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val primaryExternal = Environment.getExternalStorageDirectory()
|
||||||
|
val storageDir = File("/storage")
|
||||||
|
// Pre-seed stack with /storage so Back from primary storage goes there (USB drives etc.)
|
||||||
|
if (storageDir.exists() && storageDir.canonicalPath != primaryExternal.canonicalPath) {
|
||||||
|
directoryStack.addLast(storageDir)
|
||||||
|
}
|
||||||
|
showDirectory(primaryExternal)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateBack() {
|
||||||
|
if (directoryStack.size > 1) {
|
||||||
|
directoryStack.removeLast()
|
||||||
|
val parent = directoryStack.removeLast()
|
||||||
|
showDirectory(parent)
|
||||||
|
} else {
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDirectory(dir: File) {
|
||||||
|
directoryStack.addLast(dir)
|
||||||
|
Log.v(TAG, "Showing directory: ${dir.absolutePath}")
|
||||||
|
|
||||||
|
val entries = try {
|
||||||
|
dir.listFiles()
|
||||||
|
?.sortedWith(compareBy({ !it.isDirectory }, { it.name.lowercase() }))
|
||||||
|
?: emptyList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to list directory: $e")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val names = entries.map { if (it.isDirectory) "[${it.name}]" else it.name }.toTypedArray()
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
.setTitle(dir.absolutePath)
|
||||||
|
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
builder.setMessage("No files available")
|
||||||
|
} else {
|
||||||
|
builder.setItems(names) { dialog, which ->
|
||||||
|
dialog.dismiss()
|
||||||
|
val selected = entries[which]
|
||||||
|
if (selected.isDirectory) {
|
||||||
|
showDirectory(selected)
|
||||||
|
} else {
|
||||||
|
Log.v(TAG, "Selected file: ${selected.absolutePath}")
|
||||||
|
setResult(RESULT_OK, Intent().apply {
|
||||||
|
data = Uri.fromFile(selected)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directoryStack.size > 1) {
|
||||||
|
builder.setNegativeButton("↑ Back") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
navigateBack()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setOnCancelListener {
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user