Skip to content

Commit 8600fdb

Browse files
committed
Refactor permission handling with PermissionRequester
1 parent 2c47abc commit 8600fdb

File tree

9 files changed

+163
-199
lines changed

9 files changed

+163
-199
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.v2ray.ang.dto
2+
3+
import android.Manifest
4+
import android.os.Build
5+
import androidx.annotation.RequiresApi
6+
7+
/**
8+
* Permission types used in the app, handling API level differences.
9+
*/
10+
enum class PermissionType {
11+
/** Camera permission (used for scanning QR codes) */
12+
CAMERA {
13+
override fun getPermission(): String = Manifest.permission.CAMERA
14+
},
15+
16+
/** Read storage / media permission (adapts to Android version) */
17+
READ_STORAGE {
18+
override fun getPermission(): String {
19+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
20+
Manifest.permission.READ_MEDIA_IMAGES
21+
} else {
22+
Manifest.permission.READ_EXTERNAL_STORAGE
23+
}
24+
}
25+
},
26+
27+
/** Notification permission (Android 13+) */
28+
POST_NOTIFICATIONS {
29+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
30+
override fun getPermission(): String = Manifest.permission.POST_NOTIFICATIONS
31+
};
32+
33+
/** Return the actual Android permission string */
34+
abstract fun getPermission(): String
35+
36+
/** Return a human-readable label for the permission */
37+
fun getLabel(): String {
38+
return when (this) {
39+
CAMERA -> "Camera"
40+
READ_STORAGE -> "Storage"
41+
POST_NOTIFICATIONS -> "Notification"
42+
}
43+
}
44+
}

V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
package com.v2ray.ang.ui
22

3-
import android.Manifest
43
import android.app.AlertDialog
54
import android.content.ActivityNotFoundException
65
import android.content.Intent
7-
import android.content.pm.PackageManager
8-
import android.os.Build
96
import android.os.Bundle
107
import android.util.Log
118
import androidx.activity.result.contract.ActivityResultContracts
12-
import androidx.core.content.ContextCompat
139
import androidx.core.content.FileProvider
1410
import androidx.lifecycle.lifecycleScope
1511
import com.tencent.mmkv.MMKV
@@ -26,6 +22,7 @@ import com.v2ray.ang.extension.toastSuccess
2622
import com.v2ray.ang.handler.MmkvManager
2723
import com.v2ray.ang.handler.SettingsChangeManager
2824
import com.v2ray.ang.handler.WebDavManager
25+
import com.v2ray.ang.dto.PermissionType
2926
import com.v2ray.ang.util.ZipUtil
3027
import kotlinx.coroutines.Dispatchers
3128
import kotlinx.coroutines.launch
@@ -41,19 +38,6 @@ class BackupActivity : BaseActivity() {
4138
resources.getStringArray(R.array.config_backup_options)
4239
}
4340

44-
private val requestPermissionLauncher =
45-
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
46-
if (isGranted) {
47-
try {
48-
showFileChooser()
49-
} catch (e: Exception) {
50-
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
51-
}
52-
} else {
53-
toast(R.string.toast_permission_denied)
54-
}
55-
}
56-
5741
private val createBackupFile =
5842
registerForActivityResult(ActivityResultContracts.CreateDocument("application/zip")) { uri ->
5943
if (uri != null) {
@@ -219,20 +203,8 @@ class BackupActivity : BaseActivity() {
219203
}
220204

221205
private fun restoreViaLocal() {
222-
val permission =
223-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
224-
Manifest.permission.READ_MEDIA_IMAGES
225-
} else {
226-
Manifest.permission.READ_EXTERNAL_STORAGE
227-
}
228-
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
229-
try {
230-
showFileChooser()
231-
} catch (e: Exception) {
232-
Log.e(AppConfig.TAG, "Failed to show file chooser", e)
233-
}
234-
} else {
235-
requestPermissionLauncher.launch(permission)
206+
checkAndRequestPermission(PermissionType.READ_STORAGE) {
207+
showFileChooser()
236208
}
237209
}
238210

V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import com.v2ray.ang.R
1919
import com.v2ray.ang.handler.SettingsManager
2020
import com.v2ray.ang.helper.CustomDividerItemDecoration
2121
import com.v2ray.ang.util.MyContextWrapper
22+
import com.v2ray.ang.dto.PermissionType
2223
import com.v2ray.ang.util.Utils
24+
import com.v2ray.ang.util.permissionRequester
2325

2426

2527
/**
@@ -36,7 +38,7 @@ import com.v2ray.ang.util.Utils
3638
abstract class BaseActivity : AppCompatActivity() {
3739
// Progress indicator that sits at the bottom of the toolbar
3840
private var progressBar: LinearProgressIndicator? = null
39-
41+
protected val permissionRequester = permissionRequester()
4042
override fun onCreate(savedInstanceState: Bundle?) {
4143
super.onCreate(savedInstanceState)
4244
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@@ -213,4 +215,18 @@ abstract class BaseActivity : AppCompatActivity() {
213215
return progressBar?.visibility == View.VISIBLE
214216
}
215217

218+
/**
219+
* Check if permission is granted and request it if not.
220+
* Convenience method that delegates to permissionRequester.
221+
*
222+
* @param permissionType The type of permission to check and request
223+
* @param onGranted Callback to execute when permission is granted
224+
*/
225+
protected fun checkAndRequestPermission(
226+
permissionType: PermissionType,
227+
onGranted: () -> Unit
228+
) {
229+
permissionRequester.request(permissionType, onGranted)
230+
}
231+
216232
}

V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt

Lines changed: 12 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
package com.v2ray.ang.ui
22

3-
import android.Manifest
43
import android.content.Intent
5-
import android.content.pm.PackageManager
64
import android.content.res.ColorStateList
75
import android.net.Uri
86
import android.net.VpnService
9-
import android.os.Build
107
import android.os.Bundle
118
import android.util.Log
129
import android.view.KeyEvent
@@ -35,6 +32,7 @@ import com.v2ray.ang.handler.MmkvManager
3532
import com.v2ray.ang.handler.SettingsChangeManager
3633
import com.v2ray.ang.handler.SettingsManager
3734
import com.v2ray.ang.handler.V2RayServiceManager
35+
import com.v2ray.ang.dto.PermissionType
3836
import com.v2ray.ang.util.Utils
3937
import com.v2ray.ang.viewmodel.MainViewModel
4038
import kotlinx.coroutines.Dispatchers
@@ -65,40 +63,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
6563
}
6664
}
6765

68-
// register activity result for requesting permission
69-
private val requestPermissionLauncher =
70-
registerForActivityResult(
71-
ActivityResultContracts.RequestPermission()
72-
) { isGranted: Boolean ->
73-
if (isGranted) {
74-
when (pendingAction) {
75-
Action.IMPORT_QR_CODE_CONFIG ->
76-
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
77-
78-
Action.READ_CONTENT_FROM_URI ->
79-
chooseFileForCustomConfig.launch(Intent.createChooser(Intent(Intent.ACTION_GET_CONTENT).apply {
80-
type = "*/*"
81-
addCategory(Intent.CATEGORY_OPENABLE)
82-
}, getString(R.string.title_file_chooser)))
83-
84-
Action.POST_NOTIFICATIONS -> {}
85-
else -> {}
86-
}
87-
} else {
88-
toast(R.string.toast_permission_denied)
89-
}
90-
pendingAction = Action.NONE
91-
}
92-
93-
private var pendingAction: Action = Action.NONE
94-
95-
enum class Action {
96-
NONE,
97-
IMPORT_QR_CODE_CONFIG,
98-
READ_CONTENT_FROM_URI,
99-
POST_NOTIFICATIONS
100-
}
101-
10266
private val chooseFileForCustomConfig = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
10367
val uri = it.data?.data
10468
if (it.resultCode == RESULT_OK && uri != null) {
@@ -148,11 +112,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
148112
setupViewModel()
149113
mainViewModel.reloadServerList()
150114

151-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
152-
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
153-
pendingAction = Action.POST_NOTIFICATIONS
154-
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
155-
}
115+
checkAndRequestPermission(PermissionType.POST_NOTIFICATIONS) {
156116
}
157117
}
158118

@@ -416,12 +376,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
416376
* import config from qrcode
417377
*/
418378
private fun importQRcode(): Boolean {
419-
val permission = Manifest.permission.CAMERA
420-
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
379+
checkAndRequestPermission(PermissionType.CAMERA) {
421380
scanQRCodeForConfig.launch(Intent(this, ScannerActivity::class.java))
422-
} else {
423-
pendingAction = Action.IMPORT_QR_CODE_CONFIG
424-
requestPermissionLauncher.launch(permission)
425381
}
426382
return true
427383
}
@@ -475,7 +431,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
475431
*/
476432
private fun importConfigLocal(): Boolean {
477433
try {
478-
showFileChooser()
434+
checkAndRequestPermission(PermissionType.READ_STORAGE) {
435+
showFileChooser()
436+
}
479437
} catch (e: Exception) {
480438
Log.e(AppConfig.TAG, "Failed to import config from local file", e)
481439
return false
@@ -596,40 +554,19 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
596554
intent.type = "*/*"
597555
intent.addCategory(Intent.CATEGORY_OPENABLE)
598556

599-
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
600-
Manifest.permission.READ_MEDIA_IMAGES
601-
} else {
602-
Manifest.permission.READ_EXTERNAL_STORAGE
603-
}
604-
605-
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
606-
pendingAction = Action.READ_CONTENT_FROM_URI
607-
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
608-
} else {
609-
requestPermissionLauncher.launch(permission)
610-
}
557+
chooseFileForCustomConfig.launch(Intent.createChooser(intent, getString(R.string.title_file_chooser)))
611558
}
612559

613560
/**
614561
* read content from uri
615562
*/
616563
private fun readContentFromUri(uri: Uri) {
617-
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
618-
Manifest.permission.READ_MEDIA_IMAGES
619-
} else {
620-
Manifest.permission.READ_EXTERNAL_STORAGE
621-
}
622-
623-
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
624-
try {
625-
contentResolver.openInputStream(uri).use { input ->
626-
importBatchConfig(input?.bufferedReader()?.readText())
627-
}
628-
} catch (e: Exception) {
629-
Log.e(AppConfig.TAG, "Failed to read content from URI", e)
564+
try {
565+
contentResolver.openInputStream(uri).use { input ->
566+
importBatchConfig(input?.bufferedReader()?.readText())
630567
}
631-
} else {
632-
requestPermissionLauncher.launch(permission)
568+
} catch (e: Exception) {
569+
Log.e(AppConfig.TAG, "Failed to read content from URI", e)
633570
}
634571
}
635572

V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.v2ray.ang.ui
22

3-
import android.Manifest
43
import android.annotation.SuppressLint
54
import android.content.Intent
65
import android.os.Bundle
@@ -16,13 +15,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
1615
import com.v2ray.ang.AppConfig
1716
import com.v2ray.ang.R
1817
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
19-
import com.v2ray.ang.extension.toast
2018
import com.v2ray.ang.extension.toastError
2119
import com.v2ray.ang.extension.toastSuccess
2220
import com.v2ray.ang.handler.MmkvManager
2321
import com.v2ray.ang.handler.SettingsManager
2422
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
2523
import com.v2ray.ang.util.JsonUtil
24+
import com.v2ray.ang.dto.PermissionType
2625
import com.v2ray.ang.util.Utils
2726
import com.v2ray.ang.viewmodel.RoutingSettingsViewModel
2827
import kotlinx.coroutines.Dispatchers
@@ -43,16 +42,6 @@ class RoutingSettingActivity : BaseActivity() {
4342
resources.getStringArray(R.array.preset_rulesets)
4443
}
4544

46-
private val requestCameraPermissionLauncher = registerForActivityResult(
47-
ActivityResultContracts.RequestPermission()
48-
) { isGranted: Boolean ->
49-
if (isGranted) {
50-
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
51-
} else {
52-
toast(R.string.toast_permission_denied)
53-
}
54-
}
55-
5645
override fun onCreate(savedInstanceState: Bundle?) {
5746
super.onCreate(savedInstanceState)
5847
//setContentView(binding.root)
@@ -88,7 +77,7 @@ class RoutingSettingActivity : BaseActivity() {
8877
R.id.add_rule -> startActivity(Intent(this, RoutingEditActivity::class.java)).let { true }
8978
R.id.import_predefined_rulesets -> importPredefined().let { true }
9079
R.id.import_rulesets_from_clipboard -> importFromClipboard().let { true }
91-
R.id.import_rulesets_from_qrcode -> requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA).let { true }
80+
R.id.import_rulesets_from_qrcode -> importQRcode()
9281
R.id.export_rulesets_to_clipboard -> export2Clipboard().let { true }
9382
else -> super.onOptionsItemSelected(item)
9483
}
@@ -160,6 +149,13 @@ class RoutingSettingActivity : BaseActivity() {
160149
.show()
161150
}
162151

152+
private fun importQRcode(): Boolean {
153+
checkAndRequestPermission(PermissionType.CAMERA) {
154+
scanQRcodeForRulesets.launch(Intent(this, ScannerActivity::class.java))
155+
}
156+
return true
157+
}
158+
163159
private fun export2Clipboard() {
164160
val rulesetList = MmkvManager.decodeRoutingRulesets()
165161
if (rulesetList.isNullOrEmpty()) {

0 commit comments

Comments
 (0)