From 8600fdba35da154d7f397491e0775c6c0e48e832 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:18:19 +0800 Subject: [PATCH] Refactor permission handling with PermissionRequester --- .../java/com/v2ray/ang/dto/PermissionType.kt | 44 ++++++++++ .../java/com/v2ray/ang/ui/BackupActivity.kt | 34 +------- .../java/com/v2ray/ang/ui/BaseActivity.kt | 18 +++- .../java/com/v2ray/ang/ui/MainActivity.kt | 87 +++---------------- .../v2ray/ang/ui/RoutingSettingActivity.kt | 22 ++--- .../com/v2ray/ang/ui/ScScannerActivity.kt | 18 +--- .../java/com/v2ray/ang/ui/ScannerActivity.kt | 26 +----- .../com/v2ray/ang/ui/UserAssetActivity.kt | 65 +++++--------- .../java/com/v2ray/ang/util/PermissionUtil.kt | 48 ++++++++++ 9 files changed, 163 insertions(+), 199 deletions(-) create mode 100644 V2rayNG/app/src/main/java/com/v2ray/ang/dto/PermissionType.kt create mode 100644 V2rayNG/app/src/main/java/com/v2ray/ang/util/PermissionUtil.kt diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/dto/PermissionType.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/PermissionType.kt new file mode 100644 index 000000000..8eb5b08e5 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/dto/PermissionType.kt @@ -0,0 +1,44 @@ +package com.v2ray.ang.dto + +import android.Manifest +import android.os.Build +import androidx.annotation.RequiresApi + +/** + * Permission types used in the app, handling API level differences. + */ +enum class PermissionType { + /** Camera permission (used for scanning QR codes) */ + CAMERA { + override fun getPermission(): String = Manifest.permission.CAMERA + }, + + /** Read storage / media permission (adapts to Android version) */ + READ_STORAGE { + override fun getPermission(): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + } + }, + + /** Notification permission (Android 13+) */ + POST_NOTIFICATIONS { + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun getPermission(): String = Manifest.permission.POST_NOTIFICATIONS + }; + + /** Return the actual Android permission string */ + abstract fun getPermission(): String + + /** Return a human-readable label for the permission */ + fun getLabel(): String { + return when (this) { + CAMERA -> "Camera" + READ_STORAGE -> "Storage" + POST_NOTIFICATIONS -> "Notification" + } + } +} \ No newline at end of file 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 c8c406d45..ec9756088 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,15 +1,11 @@ package com.v2ray.ang.ui -import android.Manifest import android.app.AlertDialog import android.content.ActivityNotFoundException import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build import android.os.Bundle import android.util.Log import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.lifecycle.lifecycleScope import com.tencent.mmkv.MMKV @@ -26,6 +22,7 @@ import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsChangeManager import com.v2ray.ang.handler.WebDavManager +import com.v2ray.ang.dto.PermissionType import com.v2ray.ang.util.ZipUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -41,19 +38,6 @@ class BackupActivity : BaseActivity() { resources.getStringArray(R.array.config_backup_options) } - private val requestPermissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - if (isGranted) { - try { - showFileChooser() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to show file chooser", e) - } - } else { - toast(R.string.toast_permission_denied) - } - } - private val createBackupFile = registerForActivityResult(ActivityResultContracts.CreateDocument("application/zip")) { uri -> if (uri != null) { @@ -219,20 +203,8 @@ class BackupActivity : BaseActivity() { } private fun restoreViaLocal() { - val permission = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { - try { - showFileChooser() - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to show file chooser", e) - } - } else { - requestPermissionLauncher.launch(permission) + checkAndRequestPermission(PermissionType.READ_STORAGE) { + showFileChooser() } } 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 9bb802298..75f155bad 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 @@ -19,7 +19,9 @@ import com.v2ray.ang.R import com.v2ray.ang.handler.SettingsManager 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.permissionRequester /** @@ -36,7 +38,7 @@ import com.v2ray.ang.util.Utils abstract class BaseActivity : AppCompatActivity() { // Progress indicator that sits at the bottom of the toolbar private var progressBar: LinearProgressIndicator? = null - + protected val permissionRequester = permissionRequester() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportActionBar?.setDisplayHomeAsUpEnabled(true) @@ -213,4 +215,18 @@ abstract class BaseActivity : AppCompatActivity() { return progressBar?.visibility == View.VISIBLE } + /** + * Check if permission is granted and request it if not. + * Convenience method that delegates to permissionRequester. + * + * @param permissionType The type of permission to check and request + * @param onGranted Callback to execute when permission is granted + */ + protected fun checkAndRequestPermission( + permissionType: PermissionType, + onGranted: () -> Unit + ) { + permissionRequester.request(permissionType, onGranted) + } + } 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 ac9a1eebb..4ead2e06b 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 @@ -1,12 +1,9 @@ package com.v2ray.ang.ui -import android.Manifest import android.content.Intent -import android.content.pm.PackageManager import android.content.res.ColorStateList import android.net.Uri import android.net.VpnService -import android.os.Build import android.os.Bundle import android.util.Log import android.view.KeyEvent @@ -35,6 +32,7 @@ import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsChangeManager import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.handler.V2RayServiceManager +import com.v2ray.ang.dto.PermissionType import com.v2ray.ang.util.Utils import com.v2ray.ang.viewmodel.MainViewModel import kotlinx.coroutines.Dispatchers @@ -65,40 +63,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } } - // register activity result for requesting permission - private val requestPermissionLauncher = - registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - when (pendingAction) { - Action.IMPORT_QR_CODE_CONFIG -> - scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) - - Action.READ_CONTENT_FROM_URI -> - chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply { - type = "*/*" - addCategory(Intent.CATEGORY_OPENABLE) - }, getString(R.string.title_file_chooser))) - - Action.POST_NOTIFICATIONS -> {} - else -> {} - } - } else { - toast(R.string.toast_permission_denied) - } - pendingAction = Action.NONE - } - - private var pendingAction: Action = Action.NONE - - enum class Action { - NONE, - IMPORT_QR_CODE_CONFIG, - READ_CONTENT_FROM_URI, - POST_NOTIFICATIONS - } - private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { val uri = it.data?.data if (it.resultCode == RESULT_OK && uri != null) { @@ -148,11 +112,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList setupViewModel() mainViewModel.reloadServerList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - pendingAction = Action.POST_NOTIFICATIONS - requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) - } + checkAndRequestPermission(PermissionType.POST_NOTIFICATIONS) { } } @@ -416,12 +376,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList * import config from qrcode */ private fun importQRcode(): Boolean { - val permission = Manifest.permission.CAMERA - if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { + checkAndRequestPermission(PermissionType.CAMERA) { scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java)) - } else { - pendingAction = Action.IMPORT_QR_CODE_CONFIG - requestPermissionLauncher.launch(permission) } return true } @@ -475,7 +431,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList */ private fun importConfigLocal(): Boolean { try { - showFileChooser() + checkAndRequestPermission(PermissionType.READ_STORAGE) { + showFileChooser() + } } catch (e: Exception) { Log.e(AppConfig.TAG, "Failed to import config from local file", e) return false @@ -596,40 +554,19 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList intent.type = "*/*" intent.addCategory(Intent.CATEGORY_OPENABLE) - val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - - if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { - pendingAction = Action.READ_CONTENT_FROM_URI - chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) - } else { - requestPermissionLauncher.launch(permission) - } + chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser))) } /** * read content from uri */ private fun readContentFromUri(uri: Uri) { - val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - - if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { - try { - contentResolver.openInputStream(uri).use { input -> - importBatchConfig(input?.bufferedReader()?.readText()) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to read content from URI", e) + try { + contentResolver.openInputStream(uri).use { input -> + importBatchConfig(input?.bufferedReader()?.readText()) } - } else { - requestPermissionLauncher.launch(permission) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to read content from URI", e) } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt index 722208cc5..7f0751e8e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt @@ -1,6 +1,5 @@ package com.v2ray.ang.ui -import android.Manifest import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle @@ -16,13 +15,13 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.v2ray.ang.AppConfig import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityRoutingSettingBinding -import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.util.JsonUtil +import com.v2ray.ang.dto.PermissionType import com.v2ray.ang.util.Utils import com.v2ray.ang.viewmodel.RoutingSettingsViewModel import kotlinx.coroutines.Dispatchers @@ -43,16 +42,6 @@ class RoutingSettingActivity : BaseActivity() { resources.getStringArray(R.array.preset_rulesets) } - private val requestCameraPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java)) - } else { - toast(R.string.toast_permission_denied) - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //setContentView(binding.root) @@ -88,7 +77,7 @@ class RoutingSettingActivity : BaseActivity() { R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true } R.id.import_predefined_rulesets -> importPredefined().let { true } R.id.import_rulesets_from_clipboard -> importFromClipboard().let { true } - R.id.import_rulesets_from_qrcode -> requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA).let { true } + R.id.import_rulesets_from_qrcode -> importQRcode() R.id.export_rulesets_to_clipboard -> export2Clipboard().let { true } else -> super.onOptionsItemSelected(item) } @@ -160,6 +149,13 @@ class RoutingSettingActivity : BaseActivity() { .show() } + private fun importQRcode(): Boolean { + checkAndRequestPermission(PermissionType.CAMERA) { + scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java)) + } + return true + } + private fun export2Clipboard() { val rulesetList = MmkvManager.decodeRoutingRulesets() if (rulesetList.isNullOrEmpty()) { diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt index 9a972f937..390161afb 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt @@ -1,28 +1,16 @@ package com.v2ray.ang.ui -import android.Manifest import android.content.Intent import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts import com.v2ray.ang.R -import com.v2ray.ang.extension.toast import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.AngConfigManager +import com.v2ray.ang.dto.PermissionType class ScScannerActivity : BaseActivity() { - private val requestCameraPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - scanQRCode.launch(Intent(this, ScannerActivity::class.java)) - } else { - toast(R.string.toast_permission_denied) - finish() - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_none) @@ -30,7 +18,9 @@ class ScScannerActivity : BaseActivity() { } private fun importQRcode(): Boolean { - requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) + checkAndRequestPermission(PermissionType.CAMERA) { + scanQRCode.launch(Intent(this, ScannerActivity::class.java)) + } return true } 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 67388c7f2..6e9c678e5 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 @@ -1,21 +1,18 @@ package com.v2ray.ang.ui -import android.Manifest import android.content.Intent -import android.content.pm.PackageManager import android.graphics.BitmapFactory -import android.os.Build import android.os.Bundle import android.util.Log import android.view.Menu import android.view.MenuItem import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat 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.util.QRCodeDecoder import io.github.g00fy2.quickie.QRResult import io.github.g00fy2.quickie.ScanCustomCode @@ -47,17 +44,6 @@ class ScannerActivity : BaseActivity() { } } - private val requestPermissionLauncher = - registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - showFileChooser() - } else { - toast(R.string.toast_permission_denied) - } - } - public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -106,16 +92,8 @@ class ScannerActivity : BaseActivity() { } R.id.select_photo -> { - val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - - if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { + checkAndRequestPermission(PermissionType.READ_STORAGE) { showFileChooser() - } else { - requestPermissionLauncher.launch(permission) } true } 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 a285dd56d..afb57fc98 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 @@ -1,11 +1,9 @@ package com.v2ray.ang.ui -import android.Manifest import android.annotation.SuppressLint import android.app.AlertDialog import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle import android.provider.OpenableColumns import android.util.Log @@ -24,6 +22,7 @@ import com.v2ray.ang.extension.toastError import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsManager +import com.v2ray.ang.dto.PermissionType import com.v2ray.ang.util.Utils import com.v2ray.ang.viewmodel.UserAssetViewModel import kotlinx.coroutines.Dispatchers @@ -40,39 +39,6 @@ class UserAssetActivity : BaseActivity() { val extDir by lazy { File(Utils.userAssetPath(this)) } - private val requestStoragePermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "*/*" - intent.addCategory(Intent.CATEGORY_OPENABLE) - - try { - chooseFile.launch( - Intent.createChooser( - intent, - getString(R.string.title_file_chooser) - ) - ) - } catch (ex: android.content.ActivityNotFoundException) { - toast(R.string.toast_require_file_manager) - } - } else { - toast(R.string.toast_permission_denied) - } - } - - private val requestCameraPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java)) - } else { - toast(R.string.toast_permission_denied) - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.title_user_asset_setting)) @@ -125,14 +91,29 @@ class UserAssetActivity : BaseActivity() { } private fun showFileChooser() { - val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE + checkAndRequestPermission(PermissionType.READ_STORAGE) { + showFileChooser2() } - requestStoragePermissionLauncher.launch(permission) } + private fun showFileChooser2() { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "*/*" + intent.addCategory(Intent.CATEGORY_OPENABLE) + + 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) { @@ -182,7 +163,9 @@ class UserAssetActivity : BaseActivity() { } private fun importAssetFromQRcode(): Boolean { - requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) + checkAndRequestPermission(PermissionType.CAMERA) { + scanQRCodeForAssetURL.launch(Intent(this, ScannerActivity::class.java)) + } return true } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/PermissionUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PermissionUtil.kt new file mode 100644 index 000000000..eebf21880 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/PermissionUtil.kt @@ -0,0 +1,48 @@ +package com.v2ray.ang.util + +import android.content.pm.PackageManager +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import com.v2ray.ang.R +import com.v2ray.ang.dto.PermissionType +import com.v2ray.ang.extension.toast + +/** + * Helper for requesting permissions. + */ +class PermissionRequester(private val activity: AppCompatActivity) { + private var permissionCallback: ((Boolean) -> Unit)? = null + + private val permissionLauncher: ActivityResultLauncher = + activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + permissionCallback?.invoke(isGranted) + permissionCallback = null + } + + /** + * Check the permission and request it if not granted. + * + * @param permissionType the type of permission + * @param onGranted called when permission is granted (called immediately if already granted) + */ + fun request(permissionType: PermissionType, onGranted: () -> Unit) { + val permission = permissionType.getPermission() + if (ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED) { + onGranted() + } else { + permissionCallback = { isGranted -> + if (isGranted) { + onGranted() + } else { + val message = "${activity.getString(R.string.toast_permission_denied)} ${permissionType.getLabel()}" + activity.toast(message) + } + } + permissionLauncher.launch(permission) + } + } +} + +fun AppCompatActivity.permissionRequester() = PermissionRequester(this) \ No newline at end of file