From f6bded88dd3ddd18546889f8bbc40f1e29b56718 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:07:57 +0800 Subject: [PATCH] Refactor file chooser logic into reusable helper --- .../java/com/v2ray/ang/ui/BackupActivity.kt | 52 ++++++--------- .../java/com/v2ray/ang/ui/BaseActivity.kt | 19 +++++- .../java/com/v2ray/ang/ui/MainActivity.kt | 17 ++--- .../java/com/v2ray/ang/ui/ScannerActivity.kt | 49 ++++++-------- .../com/v2ray/ang/ui/UserAssetActivity.kt | 24 ++----- .../com/v2ray/ang/util/FileChooserUtil.kt | 65 +++++++++++++++++++ 6 files changed, 134 insertions(+), 92 deletions(-) create mode 100644 V2rayNG/app/src/main/java/com/v2ray/ang/util/FileChooserUtil.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt index ec975608..9277cdbd 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt @@ -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( diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt index 75f155ba..919183fd 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt @@ -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) + } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt index 4ead2e06..f24edabe 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt @@ -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) + } } /** diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt index 6e9c678e..a08c5524 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScannerActivity.kt @@ -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) + } } } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt index afb57fc9..ece6fba1 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt @@ -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( diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/FileChooserUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/FileChooserUtil.kt new file mode 100644 index 00000000..0819264a --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/FileChooserUtil.kt @@ -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 = + 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)