Refactor file chooser logic into reusable helper

This commit is contained in:
2dust committed 2026-01-28 17:07:57 +08:00
1 parent 8600fdba35
commit f6bded88dd
6 files changed
+134 -92

No files matched your search

@@ -1,7 +1,6 @@
package com.v2ray.ang.ui
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
@@ -16,7 +15,6 @@ import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityBackupBinding
import com.v2ray.ang.databinding.DialogWebdavBinding
import com.v2ray.ang.dto.WebDavConfig
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.extension.toastSuccess
import com.v2ray.ang.handler.MmkvManager
@@ -156,42 +154,30 @@ class BackupActivity : BaseActivity() {
}
private fun showFileChooser() {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
addCategory(Intent.CATEGORY_OPENABLE)
}
try {
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} catch (ex: ActivityNotFoundException) {
Log.e(AppConfig.TAG, "File chooser activity not found", ex)
toast(R.string.toast_require_file_manager)
}
}
private val chooseFile =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data
if (result.resultCode == RESULT_OK && uri != null) {
try {
val targetFile =
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
contentResolver.openInputStream(uri).use { input ->
targetFile.outputStream().use { fileOut ->
input?.copyTo(fileOut)
}
launchFileChooser("*/*") { uri ->
if (uri == null) {
toastError(R.string.toast_failure)
return@launchFileChooser
}
try {
val targetFile =
File(this.cacheDir.absolutePath, "${System.currentTimeMillis()}.zip")
contentResolver.openInputStream(uri).use { input ->
targetFile.outputStream().use { fileOut ->
input?.copyTo(fileOut)
}
if (restoreConfiguration(targetFile)) {
toastSuccess(R.string.toast_success)
} else {
toastError(R.string.toast_failure)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error during file restore", e)
}
if (restoreConfiguration(targetFile)) {
toastSuccess(R.string.toast_success)
} else {
toastError(R.string.toast_failure)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error during file restore", e)
toastError(R.string.toast_failure)
}
}
}
private fun backupViaLocal() {
val dateFormatted = SimpleDateFormat(
@@ -1,6 +1,7 @@
package com.v2ray.ang.ui
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
@@ -21,6 +22,7 @@ import com.v2ray.ang.helper.CustomDividerItemDecoration
import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.dto.PermissionType
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.fileChooserHelper
import com.v2ray.ang.util.permissionRequester
@@ -38,7 +40,9 @@ import com.v2ray.ang.util.permissionRequester
abstract class BaseActivity : AppCompatActivity() {
// Progress indicator that sits at the bottom of the toolbar
private var progressBar: LinearProgressIndicator? = null
protected val permissionRequester = permissionRequester()
private val permissionRequester = permissionRequester()
private val fileChooser = fileChooserHelper()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@@ -229,4 +233,17 @@ abstract class BaseActivity : AppCompatActivity() {
permissionRequester.request(permissionType, onGranted)
}
/**
* Launch file chooser with ACTION_GET_CONTENT intent.
* Convenience method that delegates to fileChooser helper.
*
* @param mimeType MIME type filter for files
* @param onResult Callback invoked with the selected file URI (null if cancelled)
*/
protected fun launchFileChooser(
mimeType: String = "*/*",
onResult: (Uri?) -> Unit
) {
fileChooser.launch(mimeType, onResult)
}
}
@@ -63,12 +63,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
}
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
readContentFromUri(uri)
}
}
private val scanQRCodeForConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
@@ -550,11 +544,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
* show file chooser
*/
private fun showFileChooser() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
launchFileChooser("*/*") { uri ->
if (uri == null) {
toastError(R.string.toast_failure)
return@launchFileChooser
}
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
readContentFromUri(uri)
}
}
/**
@@ -6,13 +6,13 @@ import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityNoneBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.dto.PermissionType
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.util.QRCodeDecoder
import io.github.g00fy2.quickie.QRResult
import io.github.g00fy2.quickie.ScanCustomCode
@@ -23,26 +23,6 @@ class ScannerActivity : BaseActivity() {
private val binding by lazy { ActivityNoneBinding.inflate(layoutInflater) }
private val scanQrCode = registerForActivityResult(ScanCustomCode(), ::handleResult)
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
try {
val inputStream = contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
if (text.isNullOrEmpty()) {
toast(R.string.toast_decoding_failed)
} else {
finished(text)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to decode QR code from file", e)
toast(R.string.toast_decoding_failed)
}
}
}
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -103,15 +83,26 @@ class ScannerActivity : BaseActivity() {
}
private fun showFileChooser() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
//intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
launchFileChooser("image/*") { uri ->
if (uri == null) {
toastError(R.string.toast_failure)
return@launchFileChooser
}
try {
val inputStream = contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
try {
chooseFile.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
} catch (ex: android.content.ActivityNotFoundException) {
toast(R.string.toast_require_file_manager)
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
if (text.isNullOrEmpty()) {
toast(R.string.toast_decoding_failed)
} else {
finished(text)
}
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Failed to decode QR code from file", e)
toast(R.string.toast_decoding_failed)
}
}
}
}
@@ -97,26 +97,12 @@ class UserAssetActivity : BaseActivity() {
}
private fun showFileChooser2() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
launchFileChooser("*/*") { uri ->
if (uri == null) {
toastError(R.string.toast_failure)
return@launchFileChooser
}
try {
chooseFile.launch(
Intent.createChooser(
intent,
getString(R.string.title_file_chooser)
)
)
} catch (ex: android.content.ActivityNotFoundException) {
toast(R.string.toast_require_file_manager)
}
}
private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data
if (result.resultCode == RESULT_OK && uri != null) {
val assetId = Utils.getUuid()
runCatching {
val assetItem = AssetUrlItem(
@@ -0,0 +1,65 @@
package com.v2ray.ang.util
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.extension.toast
/**
* Helper for choosing files using ACTION_GET_CONTENT intent.
*
*/
class FileChooserHelper(private val activity: AppCompatActivity) {
private var fileChooserCallback: ((Uri?) -> Unit)? = null
private val fileChooserLauncher: ActivityResultLauncher<Intent> =
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data
if (result.resultCode == AppCompatActivity.RESULT_OK && uri != null) {
fileChooserCallback?.invoke(uri)
} else {
fileChooserCallback?.invoke(null)
}
fileChooserCallback = null
}
/**
* Launch file chooser with ACTION_GET_CONTENT intent.
*
* @param mimeType MIME type filter for files
* @param onResult Callback invoked with the selected file URI (null if cancelled)
*/
fun launch(
mimeType: String = "*/*",
onResult: (Uri?) -> Unit
) {
fileChooserCallback = onResult
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = mimeType
addCategory(Intent.CATEGORY_OPENABLE)
}
try {
fileChooserLauncher.launch(
Intent.createChooser(intent, activity.getString(R.string.title_file_chooser))
)
} catch (ex: android.content.ActivityNotFoundException) {
Log.e(AppConfig.TAG, "File chooser activity not found", ex)
activity.toast(R.string.toast_require_file_manager)
fileChooserCallback?.invoke(null)
fileChooserCallback = null
}
}
}
/**
* Extension function to create a FileChooserHelper instance.
*/
fun AppCompatActivity.fileChooserHelper() = FileChooserHelper(this)