@@ -2,6 +2,7 @@ package io.trtc.tuikit.atomicx.permission
22
33import android.Manifest
44import android.app.Activity
5+ import android.content.Context
56import android.content.Intent
67import android.content.pm.PackageManager
78import android.net.Uri
@@ -24,6 +25,8 @@ class PermissionHandler(
2425 companion object {
2526 private const val PERMISSION_REQUEST_CODE = 9527
2627 private const val OVERLAY_PERMISSION_REQUEST_CODE = 9528
28+ private const val PREFS_NAME = " atomic_x_permissions"
29+ private const val KEY_EVER_REQUESTED_PREFIX = " ever_requested_"
2730
2831 // Permission identifiers from Dart
2932 private const val PERMISSION_CAMERA = " camera"
@@ -38,9 +41,39 @@ class PermissionHandler(
3841 private var activity: Activity ? = null
3942 private var pendingResult: MethodChannel .Result ? = null
4043 private var requestedPermissionTypes: List <String >? = null
41- private var requestedAndroidPermissions: List <String >? = null
4244 private var pendingOverlayPermissionTypes: List <String >? = null
4345
46+ private val prefs by lazy {
47+ pluginBinding.applicationContext.getSharedPreferences(PREFS_NAME , Context .MODE_PRIVATE )
48+ }
49+
50+ /* *
51+ * Check if a permission type has ever been requested via [requestPermissions].
52+ * Used by [checkPermissionStatus] to avoid false-positive
53+ * `permanentlyDenied` on devices (e.g. Huawei) where
54+ * `shouldShowRequestPermissionRationale` returns `false` even before
55+ * the permission has ever been requested.
56+ */
57+ private fun hasEverBeenRequested (permissionType : String ): Boolean {
58+ return prefs.getBoolean(" $KEY_EVER_REQUESTED_PREFIX$permissionType " , false )
59+ }
60+
61+ /* *
62+ * Mark permission types as having been requested at least once.
63+ *
64+ * Called in [onRequestPermissionsResult] — i.e. only after the system
65+ * permission dialog has returned a result and the user has made a choice.
66+ * This ensures that if the user kills the app before interacting with
67+ * the dialog, the permission is NOT marked as "ever requested".
68+ */
69+ private fun markAsRequested (permissionTypes : List <String >) {
70+ val editor = prefs.edit()
71+ for (type in permissionTypes) {
72+ editor.putBoolean(" $KEY_EVER_REQUESTED_PREFIX$type " , true )
73+ }
74+ editor.apply ()
75+ }
76+
4477 fun setActivity (activity : Activity ? ) {
4578 this .activity = activity
4679 }
@@ -139,17 +172,13 @@ class PermissionHandler(
139172 }
140173
141174 /* *
142- * Core logic for resolving a permission type's status.
175+ * Core logic for resolving a permission type's raw status.
143176 *
144- * @param permissionType The permission type identifier from Dart.
145- * @param checkRequestedPermissions When `true`, only reports `permanentlyDenied`
146- * for permissions tracked in [requestedAndroidPermissions] (used after a
147- * request flow to avoid false positives). When `false`, reports
148- * `permanentlyDenied` purely based on `!isGranted && !shouldShowRationale`
149- * (used for standalone check calls; the Dart layer cross-validates with a
150- * subsequent request() to filter out false positives).
177+ * Reports `permanentlyDenied` purely based on `!isGranted && !shouldShowRationale`.
178+ * Callers (e.g. [checkPermissionStatus]) are responsible for filtering out
179+ * false positives via [hasEverBeenRequested].
151180 */
152- private fun resolvePermissionStatus (permissionType : String , checkRequestedPermissions : Boolean ): String {
181+ private fun resolvePermissionStatus (permissionType : String ): String {
153182 // Handle overlay permissions separately
154183 if (isOverlayPermission(permissionType)) {
155184 return if (canDrawOverlays()) " granted" else " denied"
@@ -184,13 +213,11 @@ class PermissionHandler(
184213 return " denied"
185214 }
186215
187- // Check if any permission is permanently denied.
188- // When checkRequestedPermissions is true, only report permanentlyDenied for
189- // permissions that have been requested in the current session.
216+ // Check if any permission is permanently denied
190217 val anyPermanentlyDenied = androidPermissions.any { permission ->
191218 val isGranted = ContextCompat .checkSelfPermission(context, permission) == PackageManager .PERMISSION_GRANTED
192219 val shouldShow = ActivityCompat .shouldShowRequestPermissionRationale(currentActivity, permission)
193- ! isGranted && ! shouldShow && ( ! checkRequestedPermissions || requestedAndroidPermissions?.contains(permission) == true )
220+ ! isGranted && ! shouldShow
194221 }
195222
196223 if (anyPermanentlyDenied) {
@@ -200,34 +227,6 @@ class PermissionHandler(
200227 return " denied"
201228 }
202229
203- /* *
204- * Get the status of a permission type after a requestPermissions flow.
205- *
206- * Uses [requestedAndroidPermissions] to avoid false positives:
207- * `shouldShowRequestPermissionRationale` returns false for both "never asked"
208- * and "permanently denied". By checking against `requestedAndroidPermissions`,
209- * we only report `permanentlyDenied` for permissions that have actually been
210- * requested in the current session.
211- */
212- private fun getStatusAfterRequest (permissionType : String ): String {
213- return resolvePermissionStatus(permissionType, checkRequestedPermissions = true )
214- }
215-
216- /* *
217- * Check the status of a permission type (used for standalone check calls).
218- *
219- * Unlike [getPermissionTypeStatus], this method does NOT rely on
220- * [requestedAndroidPermissions]. It reports `permanentlyDenied` purely based
221- * on `!isGranted && !shouldShowRationale`, which may produce false positives
222- * when the permission has never been requested.
223- *
224- * The Dart layer uses this to get a preliminary signal, then cross-validates
225- * with a subsequent `request()` call to filter out false positives.
226- */
227- private fun checkPermissionStatusInternal (permissionType : String ): String {
228- return resolvePermissionStatus(permissionType, checkRequestedPermissions = false )
229- }
230-
231230 fun requestPermissions (permissionTypes : List <String >, result : MethodChannel .Result ) {
232231 val currentActivity = activity
233232 if (currentActivity == null ) {
@@ -259,7 +258,6 @@ class PermissionHandler(
259258 }
260259
261260 requestedPermissionTypes = regularPermissions
262- requestedAndroidPermissions = androidPermissions
263261 pendingResult = result
264262
265263 if (androidPermissions.isNotEmpty()) {
@@ -342,7 +340,7 @@ class PermissionHandler(
342340
343341 // Add regular permissions status
344342 for (permissionType in regularPermissions) {
345- resultMap[permissionType] = getStatusAfterRequest (permissionType)
343+ resultMap[permissionType] = checkPermissionStatus (permissionType)
346344 }
347345
348346 // Add overlay permissions status
@@ -368,8 +366,26 @@ class PermissionHandler(
368366 }
369367 }
370368
369+ /* *
370+ * Check the status of a permission type (used for standalone check calls).
371+ *
372+ * Only reports `permanentlyDenied` when the permission has been requested
373+ * at least once before (persisted via SharedPreferences). This avoids
374+ * false positives on devices like Huawei where
375+ * `shouldShowRequestPermissionRationale` returns `false` even for
376+ * never-requested permissions.
377+ *
378+ * The Dart layer uses this to get a preliminary signal, then cross-validates
379+ * with a subsequent `request()` call to filter out false positives.
380+ */
371381 fun checkPermissionStatus (permissionType : String ): String {
372- return checkPermissionStatusInternal(permissionType)
382+ val status = resolvePermissionStatus(permissionType)
383+ // If resolve says permanentlyDenied but the permission has never been
384+ // requested, it is a false positive — report as denied instead.
385+ if (status == " permanentlyDenied" && ! hasEverBeenRequested(permissionType)) {
386+ return " denied"
387+ }
388+ return status
373389 }
374390
375391 override fun onRequestPermissionsResult (
@@ -391,14 +407,20 @@ class PermissionHandler(
391407 return true
392408 }
393409
410+ // The system dialog has returned a result — the user made a choice.
411+ // Persist that these permission types have been requested at least once,
412+ // so that future checkPermissionStatus() calls can accurately
413+ // distinguish "never asked" from "permanently denied".
414+ markAsRequested(permissionTypes)
415+
394416 // If we have pending overlay permissions, request them now
395417 if (! overlayPermissions.isNullOrEmpty()) {
396418 requestOverlayPermissionAfterRegular(permissionTypes, overlayPermissions, result)
397419 return true
398420 }
399421
400422 // Build result map based on permission types
401- val resultMap = permissionTypes.associateWith { getStatusAfterRequest (it) }
423+ val resultMap = permissionTypes.associateWith { checkPermissionStatus (it) }
402424
403425 result.success(resultMap)
404426 clearPendingRequest()
@@ -457,7 +479,6 @@ class PermissionHandler(
457479 private fun clearPendingRequest () {
458480 pendingResult = null
459481 requestedPermissionTypes = null
460- requestedAndroidPermissions = null
461482 pendingOverlayPermissionTypes = null
462483 }
463484}
0 commit comments