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:
+80
-40
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user