feat(android): add launcher shortcuts and one-step on-demand access

Changelog: Launcher shortcuts and one-step on-demand access
This commit is contained in:
chen08209
2026-10-08 02:49:08 +08:00
parent 94a25f4827
commit d3fa2252b0
53 changed files with 2089 additions and 913 deletions
@@ -15,6 +15,7 @@ import android.os.Handler
import android.os.Looper
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@@ -33,6 +34,7 @@ class WifiSsidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private var connectivityManager: ConnectivityManager? = null
private var activityBinding: ActivityPluginBinding? = null
private var pendingPermissionResult: Result? = null
private var rationaleBeforeRequest: Map<String, Boolean> = emptyMap()
private var wifiNetworkCallback: ConnectivityManager.NetworkCallback? = null
private val wifiInfoByNetwork = ConcurrentHashMap<Network, WifiInfo>()
private val pendingSsidResults = mutableListOf<Result>()
@@ -53,23 +55,18 @@ class WifiSsidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.error(ERROR_UNAVAILABLE, "Activity not available", null)
return@RequestPermissionsResultListener true
}
// R+ dialogs never offer "Allow all the time"; asking for it alone opens Settings.
if (
requestCode == REQUEST_CODE_LOCATION &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
hasForegroundLocation(currentActivity) &&
!hasBackgroundLocation(currentActivity) &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.R
!hasBackgroundLocation(currentActivity)
) {
requestBackgroundLocation(currentActivity)
return@RequestPermissionsResultListener true
}
pendingPermissionResult = null
val state = permissionState(currentActivity, afterRequest = true)
if (hasForegroundLocation(currentActivity)) {
refreshWifiNetworkCallback()
} else {
unregisterWifiNetworkCallback()
}
result.success(state)
result.success(settleRequest(currentActivity))
true
}
@@ -80,8 +77,10 @@ class WifiSsidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private const val METHOD_REQUEST_PERMISSION = "requestPermission"
private const val ERROR_UNAVAILABLE = "UNAVAILABLE"
private const val ERROR_IN_PROGRESS = "IN_PROGRESS"
private const val REQUEST_CODE_LOCATION = 1001
private const val REQUEST_CODE_BACKGROUND_LOCATION = 1002
// Every plugin on the activity sees every result; stay clear of the app's codes.
private const val REQUEST_CODE_LOCATION = 2101
private const val REQUEST_CODE_BACKGROUND_LOCATION = 2102
private const val DENIED_PERMISSIONS_PREFERENCES = "wifi_ssid_denied_permissions"
private const val SSID_TIMEOUT_MILLIS = 3_000L
// Values must match WifiSsidPermission enum index in Dart
@@ -148,12 +147,11 @@ class WifiSsidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.error(ERROR_UNAVAILABLE, "Context not available", null)
return
}
if (hasForegroundLocation(ctx)) {
refreshWifiNetworkCallback()
} else {
unregisterWifiNetworkCallback()
}
result.success(permissionState(ctx))
refreshWifiNetworkCallback()
forgetGrantedDenials(ctx)
result.success(
if (missingPermission(ctx) == null) PERMISSION_GRANTED else PERMISSION_DENIED,
)
}
private fun requestPermission(result: Result) {
@@ -165,8 +163,9 @@ class WifiSsidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.error(ERROR_UNAVAILABLE, "Context not available", null)
return
}
if (permissionState(ctx) == PERMISSION_GRANTED) {
if (missingPermission(ctx) == null) {
refreshWifiNetworkCallback()
forgetGrantedDenials(ctx)
result.success(PERMISSION_GRANTED)
return
}
@@ -174,17 +173,15 @@ class WifiSsidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
result.error(ERROR_IN_PROGRESS, "A permission request is already active", null)
return
}
// R+ can't grant background location from a runtime dialog once foreground is held.
if (hasForegroundLocation(ctx) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
refreshWifiNetworkCallback()
result.success(permissionState(ctx, afterRequest = true))
return
}
pendingPermissionResult = result
if (hasForegroundLocation(ctx)) {
requestBackgroundLocation(act)
return
} else {
requestForegroundLocation(act)
}
}
private fun requestForegroundLocation(activity: Activity) {
// Q accepts the background permission in the same dialog; R+ rejects the
// combined request and needs it asked separately once foreground is granted.
val permissions = if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
@@ -199,17 +196,68 @@ class WifiSsidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
Manifest.permission.ACCESS_COARSE_LOCATION,
)
}
ActivityCompat.requestPermissions(act, permissions, REQUEST_CODE_LOCATION)
launchPermissionRequest(activity, permissions, REQUEST_CODE_LOCATION)
}
private fun requestBackgroundLocation(activity: Activity) {
ActivityCompat.requestPermissions(
launchPermissionRequest(
activity,
arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
REQUEST_CODE_BACKGROUND_LOCATION,
)
}
private fun launchPermissionRequest(
activity: Activity,
permissions: Array<String>,
requestCode: Int,
) {
rationaleBeforeRequest = permissions.associateWith {
ActivityCompat.shouldShowRequestPermissionRationale(activity, it)
}
ActivityCompat.requestPermissions(activity, permissions, requestCode)
}
private fun settleRequest(activity: Activity): Int {
val rationaleBefore = rationaleBeforeRequest
rationaleBeforeRequest = emptyMap()
refreshWifiNetworkCallback()
forgetGrantedDenials(activity)
val missing = missingPermission(activity) ?: return PERMISSION_GRANTED
return resolveDenial(activity, missing, rationaleBefore[missing] == true)
}
// Rationale is false both before the first prompt and after a permanent denial;
// how it moved across the request and a persisted earlier denial tell them apart.
private fun resolveDenial(
activity: Activity,
permission: String,
rationaleBefore: Boolean,
): Int {
val preferences = deniedPermissions(activity)
val deniedBefore = preferences.getBoolean(permission, false)
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
preferences.edit { putBoolean(permission, true) }
return PERMISSION_DENIED
}
if (rationaleBefore) {
preferences.edit { putBoolean(permission, true) }
return PERMISSION_PERMANENTLY_DENIED
}
return if (deniedBefore) PERMISSION_PERMANENTLY_DENIED else PERMISSION_DENIED
}
private fun forgetGrantedDenials(context: Context) {
val preferences = deniedPermissions(context)
val granted = preferences.all.keys.filter { isGranted(context, it) }
if (granted.isNotEmpty()) {
preferences.edit { granted.forEach(::remove) }
}
}
private fun deniedPermissions(context: Context) =
context.getSharedPreferences(DENIED_PERMISSIONS_PREFERENCES, Context.MODE_PRIVATE)
private fun isGranted(context: Context, permission: String): Boolean =
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
@@ -222,19 +270,10 @@ class WifiSsidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ||
isGranted(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
private fun permissionState(context: Context, afterRequest: Boolean = false): Int {
val missing = when {
!hasForegroundLocation(context) -> Manifest.permission.ACCESS_FINE_LOCATION
!hasBackgroundLocation(context) -> Manifest.permission.ACCESS_BACKGROUND_LOCATION
else -> return PERMISSION_GRANTED
}
if (!afterRequest) return PERMISSION_DENIED
val activity = activity ?: return PERMISSION_DENIED
return if (ActivityCompat.shouldShowRequestPermissionRationale(activity, missing)) {
PERMISSION_DENIED
} else {
PERMISSION_PERMANENTLY_DENIED
}
private fun missingPermission(context: Context): String? = when {
!hasForegroundLocation(context) -> Manifest.permission.ACCESS_FINE_LOCATION
!hasBackgroundLocation(context) -> Manifest.permission.ACCESS_BACKGROUND_LOCATION
else -> null
}
private fun getSsid(result: Result) {
@@ -388,6 +427,7 @@ class WifiSsidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
activityBinding = null
activity = null
if (cancelPermissionRequest) {
rationaleBeforeRequest = emptyMap()
completePendingPermissionRequest(
"Activity detached before permission request completed",
)
@@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
/// The platform permission state required to read the current Wi-Fi SSID.
enum WifiSsidPermission { granted, denied, permanentlyDenied }
/// Provides access to the current Wi-Fi SSID and its required permission.
class WifiSsidManager {
WifiSsidManager._();
@@ -19,13 +18,11 @@ class WifiSsidManager {
return _channel.invokeMethod<String>(_getSsidMethod);
}
/// Checks whether the required platform permission has been granted.
Future<WifiSsidPermission> checkPermission() async {
final result = await _channel.invokeMethod<int>(_checkPermissionMethod);
return _decodePermission(result);
}
/// Requests the required platform permission from the user.
Future<WifiSsidPermission> requestPermission() async {
final result = await _channel.invokeMethod<int>(_requestPermissionMethod);
return _decodePermission(result);