perf: short-circuit granted mic permission checks
This commit is contained in:
@@ -78,6 +78,8 @@ const String methodPermissionStateChanged = 'permissionStateChanged';
|
||||
/// Trace: SDD-106 §1.
|
||||
@visibleForTesting
|
||||
const String methodRequestRecordAudio = 'requestRecordAudio';
|
||||
@visibleForTesting
|
||||
const String methodRequestStartupPermissions = 'requestStartupPermissions';
|
||||
|
||||
/// Outbound (Dart → Kotlin) method that deep-links to the application's
|
||||
/// Android Settings page so the user can re-grant a permanently-denied
|
||||
@@ -109,6 +111,15 @@ enum AndroidRecordAudioPermissionState {
|
||||
unknown,
|
||||
}
|
||||
|
||||
enum AndroidPermissionKind {
|
||||
recordAudio('android.permission.RECORD_AUDIO'),
|
||||
bluetoothConnect('android.permission.BLUETOOTH_CONNECT'),
|
||||
postNotifications('android.permission.POST_NOTIFICATIONS');
|
||||
|
||||
const AndroidPermissionKind(this.permission);
|
||||
final String permission;
|
||||
}
|
||||
|
||||
/// Maps the Kotlin-side `PermissionState` string into the Dart enum.
|
||||
AndroidRecordAudioPermissionState _parseState(String? raw) {
|
||||
switch (raw) {
|
||||
@@ -157,6 +168,16 @@ class AndroidPermissionsService {
|
||||
: AndroidRecordAudioPermissionState.granted,
|
||||
);
|
||||
|
||||
final Map<AndroidPermissionKind, ValueNotifier<AndroidRecordAudioPermissionState>>
|
||||
_extraStates = {
|
||||
for (final kind in AndroidPermissionKind.values)
|
||||
kind: ValueNotifier<AndroidRecordAudioPermissionState>(
|
||||
_isAndroid
|
||||
? AndroidRecordAudioPermissionState.unknown
|
||||
: AndroidRecordAudioPermissionState.granted,
|
||||
),
|
||||
};
|
||||
|
||||
bool _started = false;
|
||||
|
||||
/// Latest known [RECORD_AUDIO] state. Defaults to
|
||||
@@ -191,16 +212,29 @@ class AndroidPermissionsService {
|
||||
if (call.method != methodPermissionStateChanged) return null;
|
||||
final args = call.arguments;
|
||||
if (args is! Map) return null;
|
||||
// We currently track RECORD_AUDIO only. POST_NOTIFICATIONS (SDD-107
|
||||
// §6) is on the same channel by design and will route through a
|
||||
// sibling listenable when that work lands.
|
||||
final permission = args['permission'];
|
||||
if (permission != 'android.permission.RECORD_AUDIO') return null;
|
||||
if (permission is! String) return null;
|
||||
final state = _parseState(args['state'] as String?);
|
||||
_state.value = state;
|
||||
if (permission == AndroidPermissionKind.recordAudio.permission) {
|
||||
_state.value = state;
|
||||
}
|
||||
for (final entry in _extraStates.entries) {
|
||||
if (entry.key.permission == permission) {
|
||||
entry.value.value = state;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ValueListenable<AndroidRecordAudioPermissionState> permissionState(
|
||||
AndroidPermissionKind kind,
|
||||
) {
|
||||
if (kind == AndroidPermissionKind.recordAudio) {
|
||||
return _state;
|
||||
}
|
||||
return _extraStates[kind]!;
|
||||
}
|
||||
|
||||
/// Request the system permission. Invokes the Kotlin requester and
|
||||
/// then, if the platform synchronously resolves the request, returns
|
||||
/// the resolved state without parking for the listener; otherwise
|
||||
@@ -215,6 +249,9 @@ class AndroidPermissionsService {
|
||||
if (ch == null) {
|
||||
return AndroidRecordAudioPermissionState.granted;
|
||||
}
|
||||
if (_state.value == AndroidRecordAudioPermissionState.granted) {
|
||||
return AndroidRecordAudioPermissionState.granted;
|
||||
}
|
||||
// Snapshot the pre-invocation state. New Android hosts return the
|
||||
// resolved Kotlin PermissionState string from requestRecordAudio;
|
||||
// older/test hosts may still return null and rely only on the
|
||||
@@ -272,6 +309,39 @@ class AndroidPermissionsService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<AndroidPermissionKind, AndroidRecordAudioPermissionState>>
|
||||
ensureStartupPermissions() async {
|
||||
final ch = _channel;
|
||||
if (ch == null) {
|
||||
return {
|
||||
for (final kind in AndroidPermissionKind.values)
|
||||
kind: AndroidRecordAudioPermissionState.granted,
|
||||
};
|
||||
}
|
||||
Map<dynamic, dynamic>? returned;
|
||||
try {
|
||||
returned = await ch.invokeMethod<Map<dynamic, dynamic>>(
|
||||
methodRequestStartupPermissions,
|
||||
);
|
||||
} catch (_) {
|
||||
return {
|
||||
for (final kind in AndroidPermissionKind.values)
|
||||
kind: permissionState(kind).value,
|
||||
};
|
||||
}
|
||||
final resolved = <AndroidPermissionKind, AndroidRecordAudioPermissionState>{};
|
||||
for (final kind in AndroidPermissionKind.values) {
|
||||
final parsed = _parseState(returned?[kind.permission] as String?);
|
||||
if (kind == AndroidPermissionKind.recordAudio) {
|
||||
_state.value = parsed;
|
||||
} else {
|
||||
_extraStates[kind]!.value = parsed;
|
||||
}
|
||||
resolved[kind] = parsed;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/// Deep-link to the system app settings page for the permanently
|
||||
/// denied case (SDD-106 §3). On non-Android, a no-op.
|
||||
Future<void> openAppSettings() async {
|
||||
@@ -297,5 +367,8 @@ class AndroidPermissionsService {
|
||||
void dispose() {
|
||||
stop();
|
||||
_state.dispose();
|
||||
for (final notifier in _extraStates.values) {
|
||||
notifier.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,9 @@ class IosPermissionsService {
|
||||
Future<AndroidRecordAudioPermissionState> ensureRecordAudio() async {
|
||||
final ch = _channel;
|
||||
if (ch == null) return AndroidRecordAudioPermissionState.granted;
|
||||
if (_state.value == AndroidRecordAudioPermissionState.granted) {
|
||||
return AndroidRecordAudioPermissionState.granted;
|
||||
}
|
||||
try {
|
||||
final state = _parseState(
|
||||
await ch.invokeMethod<String>(methodRequestMicrophonePermission),
|
||||
|
||||
Reference in New Issue
Block a user