Add Bluetooth permission and adapter state handling for Android and iOS
This commit is contained in:
+69
@@ -0,0 +1,69 @@
|
|||||||
|
# Uncomment this line to define a global platform for your project
|
||||||
|
# platform :ios, '15.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
|
||||||
|
# permission_handler compiles only the permissions enabled here. OpenMotion
|
||||||
|
# needs Bluetooth only; every other permission is compiled out so App Store
|
||||||
|
# review does not see unused privacy-sensitive APIs.
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||||
|
'$(inherited)',
|
||||||
|
'PERMISSION_BLUETOOTH=1',
|
||||||
|
'PERMISSION_EVENTS=0',
|
||||||
|
'PERMISSION_EVENTS_FULL_ACCESS=0',
|
||||||
|
'PERMISSION_REMINDERS=0',
|
||||||
|
'PERMISSION_CONTACTS=0',
|
||||||
|
'PERMISSION_CAMERA=0',
|
||||||
|
'PERMISSION_MICROPHONE=0',
|
||||||
|
'PERMISSION_SPEECH_RECOGNIZER=0',
|
||||||
|
'PERMISSION_PHOTOS=0',
|
||||||
|
'PERMISSION_LOCATION=0',
|
||||||
|
'PERMISSION_LOCATION_WHENINUSE=0',
|
||||||
|
'PERMISSION_NOTIFICATIONS=0',
|
||||||
|
'PERMISSION_MEDIA_LIBRARY=0',
|
||||||
|
'PERMISSION_SENSORS=0',
|
||||||
|
'PERMISSION_APP_TRACKING_TRANSPARENCY=0',
|
||||||
|
'PERMISSION_CRITICAL_ALERTS=0',
|
||||||
|
'PERMISSION_ASSISTANT=0',
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,7 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import '../models/scooter_device.dart';
|
import '../models/scooter_device.dart';
|
||||||
import '../scooters/apollo_scooter.dart';
|
import '../scooters/apollo_scooter.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import '../services/ble_client.dart';
|
import '../services/ble_client.dart';
|
||||||
|
import '../services/ble_permissions.dart';
|
||||||
import '../services/demo_ble_client.dart';
|
import '../services/demo_ble_client.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
import 'scooter_screen.dart';
|
import 'scooter_screen.dart';
|
||||||
@@ -16,8 +20,14 @@ class ScanScreen extends StatefulWidget {
|
|||||||
State<ScanScreen> createState() => _ScanScreenState();
|
State<ScanScreen> createState() => _ScanScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ScanScreenState extends State<ScanScreen> {
|
/// What is stopping us from scanning, if anything.
|
||||||
|
enum _Gate { checking, permissionDenied, permissionPermanentlyDenied, bluetoothOff, unsupported, ready }
|
||||||
|
|
||||||
|
class _ScanScreenState extends State<ScanScreen> with WidgetsBindingObserver {
|
||||||
Stream<List<ScooterDevice>>? _scan;
|
Stream<List<ScooterDevice>>? _scan;
|
||||||
|
_Gate _gate = _Gate.checking;
|
||||||
|
StreamSubscription<BleAdapterState>? _adapterSub;
|
||||||
|
bool _requesting = false;
|
||||||
|
|
||||||
/// Development path: list every BLE device so a scooter that does not
|
/// Development path: list every BLE device so a scooter that does not
|
||||||
/// advertise F1F0/F2F0 can still be selected and classified after GATT
|
/// advertise F1F0/F2F0 can still be selected and classified after GATT
|
||||||
@@ -27,10 +37,86 @@ class _ScanScreenState extends State<ScanScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_scan = widget.ble.scan();
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
_prepare(request: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _restart() => setState(() => _scan = widget.ble.scan());
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_adapterSub?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-check when coming back from system settings or the Bluetooth toggle.
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed && _gate != _Gate.ready) {
|
||||||
|
_prepare(request: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Permission first, then adapter state, then scan.
|
||||||
|
Future<void> _prepare({required bool request}) async {
|
||||||
|
if (_requesting) return;
|
||||||
|
_requesting = true;
|
||||||
|
try {
|
||||||
|
final access = request ? await BlePermissions.request() : await BlePermissions.status();
|
||||||
|
if (!mounted) return;
|
||||||
|
switch (access) {
|
||||||
|
case BleAccess.denied:
|
||||||
|
_setGate(_Gate.permissionDenied);
|
||||||
|
return;
|
||||||
|
case BleAccess.permanentlyDenied:
|
||||||
|
_setGate(_Gate.permissionPermanentlyDenied);
|
||||||
|
return;
|
||||||
|
case BleAccess.unsupported:
|
||||||
|
_setGate(_Gate.unsupported);
|
||||||
|
return;
|
||||||
|
case BleAccess.granted:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_watchAdapter();
|
||||||
|
} finally {
|
||||||
|
_requesting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _watchAdapter() {
|
||||||
|
_adapterSub ??= widget.ble.adapterState.listen((s) {
|
||||||
|
if (!mounted) return;
|
||||||
|
switch (s) {
|
||||||
|
case BleAdapterState.on:
|
||||||
|
if (_gate != _Gate.ready) {
|
||||||
|
_setGate(_Gate.ready);
|
||||||
|
_restart();
|
||||||
|
}
|
||||||
|
case BleAdapterState.off:
|
||||||
|
case BleAdapterState.unknown:
|
||||||
|
_setGate(_Gate.bluetoothOff);
|
||||||
|
case BleAdapterState.unauthorized:
|
||||||
|
_setGate(_Gate.permissionPermanentlyDenied);
|
||||||
|
case BleAdapterState.unsupported:
|
||||||
|
_setGate(_Gate.unsupported);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setGate(_Gate g) {
|
||||||
|
if (_gate == g) return;
|
||||||
|
setState(() {
|
||||||
|
_gate = g;
|
||||||
|
if (g != _Gate.ready) _scan = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _restart() {
|
||||||
|
if (_gate != _Gate.ready) {
|
||||||
|
_prepare(request: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _scan = widget.ble.scan());
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _connect(ScooterDevice device, {BleClient? ble}) async {
|
Future<void> _connect(ScooterDevice device, {BleClient? ble}) async {
|
||||||
setState(() => _scan = null);
|
setState(() => _scan = null);
|
||||||
@@ -89,7 +175,9 @@ class _ScanScreenState extends State<ScanScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _scan == null
|
child: _gate != _Gate.ready
|
||||||
|
? _gateView()
|
||||||
|
: _scan == null
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: StreamBuilder<List<ScooterDevice>>(
|
: StreamBuilder<List<ScooterDevice>>(
|
||||||
stream: _scan,
|
stream: _scan,
|
||||||
@@ -152,6 +240,54 @@ class _ScanScreenState extends State<ScanScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on _ScanScreenState {
|
||||||
|
Widget _gateView() {
|
||||||
|
final android = Platform.isAndroid;
|
||||||
|
return switch (_gate) {
|
||||||
|
_Gate.checking => const Center(child: CircularProgressIndicator()),
|
||||||
|
_Gate.permissionDenied => _Empty(
|
||||||
|
icon: Icons.bluetooth_rounded,
|
||||||
|
title: 'Bluetooth permission needed',
|
||||||
|
message: android
|
||||||
|
? 'OpenMotion needs the Nearby devices permission to find and talk to your scooter. '
|
||||||
|
'Scan results are never used for location.'
|
||||||
|
: 'OpenMotion needs Bluetooth access to find and talk to your scooter.',
|
||||||
|
action: FilledButton(
|
||||||
|
onPressed: () => _prepare(request: true),
|
||||||
|
child: const Text('Allow Bluetooth'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_Gate.permissionPermanentlyDenied => _Empty(
|
||||||
|
icon: Icons.bluetooth_disabled_rounded,
|
||||||
|
title: 'Bluetooth permission blocked',
|
||||||
|
message: android
|
||||||
|
? 'Nearby devices access was denied. Enable it for OpenMotion in system settings, then come back.'
|
||||||
|
: 'Bluetooth access is off for OpenMotion. Enable it in Settings, then come back.',
|
||||||
|
action: FilledButton(
|
||||||
|
onPressed: BlePermissions.openSettings,
|
||||||
|
child: const Text('Open settings'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_Gate.bluetoothOff => _Empty(
|
||||||
|
icon: Icons.bluetooth_disabled_rounded,
|
||||||
|
title: 'Bluetooth is off',
|
||||||
|
message: android
|
||||||
|
? 'Turn on Bluetooth to scan for your scooter.'
|
||||||
|
: 'Turn on Bluetooth in Control Center or Settings to scan for your scooter.',
|
||||||
|
action: android
|
||||||
|
? FilledButton(onPressed: widget.ble.turnOn, child: const Text('Turn on Bluetooth'))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
_Gate.unsupported => const _Empty(
|
||||||
|
icon: Icons.error_outline_rounded,
|
||||||
|
title: 'Bluetooth unavailable',
|
||||||
|
message: 'This device does not support Bluetooth Low Energy.',
|
||||||
|
),
|
||||||
|
_Gate.ready => const SizedBox.shrink(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _DeviceCard extends StatelessWidget {
|
class _DeviceCard extends StatelessWidget {
|
||||||
const _DeviceCard({required this.device, required this.onConnect});
|
const _DeviceCard({required this.device, required this.onConnect});
|
||||||
|
|
||||||
@@ -229,7 +365,8 @@ class _Empty extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.fromLTRB(24, 48, 24, 24),
|
padding: const EdgeInsets.fromLTRB(24, 48, 24, 24),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import 'protocol_log.dart';
|
|||||||
|
|
||||||
enum BleConnectionState { disconnected, connected }
|
enum BleConnectionState { disconnected, connected }
|
||||||
|
|
||||||
|
enum BleAdapterState { unknown, unsupported, off, on, unauthorized }
|
||||||
|
|
||||||
/// Thin boundary between OpenScooter and the underlying BLE plugin.
|
/// Thin boundary between OpenScooter and the underlying BLE plugin.
|
||||||
///
|
///
|
||||||
/// OpenScooter v1 supports EXACTLY ONE active scooter connection at a time.
|
/// OpenScooter v1 supports EXACTLY ONE active scooter connection at a time.
|
||||||
@@ -17,6 +19,13 @@ enum BleConnectionState { disconnected, connected }
|
|||||||
/// Protocol code never imports the BLE plugin directly, so protocol tests run
|
/// Protocol code never imports the BLE plugin directly, so protocol tests run
|
||||||
/// with `flutter test` and a fake subclass, without hardware.
|
/// with `flutter test` and a fake subclass, without hardware.
|
||||||
abstract class BleClient {
|
abstract class BleClient {
|
||||||
|
/// Current Bluetooth adapter state, emitting on change. Fakes default to on.
|
||||||
|
Stream<BleAdapterState> get adapterState => Stream.value(BleAdapterState.on);
|
||||||
|
|
||||||
|
/// Asks the OS to enable Bluetooth. Only Android supports this; elsewhere it
|
||||||
|
/// is a no-op and the UI must direct the user to system settings.
|
||||||
|
Future<void> turnOn() async {}
|
||||||
|
|
||||||
/// Streams the current set of visible devices. Scanning starts on listen
|
/// Streams the current set of visible devices. Scanning starts on listen
|
||||||
/// and stops when the subscription is cancelled.
|
/// and stops when the subscription is cancelled.
|
||||||
Stream<List<ScooterDevice>> scan();
|
Stream<List<ScooterDevice>> scan();
|
||||||
@@ -63,6 +72,27 @@ class FlutterBleClient extends BleClient {
|
|||||||
@override
|
@override
|
||||||
Stream<BleConnectionState> get connectionState => _connState.stream;
|
Stream<BleConnectionState> get connectionState => _connState.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<BleAdapterState> get adapterState =>
|
||||||
|
fbp.FlutterBluePlus.adapterState.map((s) => switch (s) {
|
||||||
|
fbp.BluetoothAdapterState.on => BleAdapterState.on,
|
||||||
|
fbp.BluetoothAdapterState.off ||
|
||||||
|
fbp.BluetoothAdapterState.turningOff ||
|
||||||
|
fbp.BluetoothAdapterState.turningOn =>
|
||||||
|
BleAdapterState.off,
|
||||||
|
fbp.BluetoothAdapterState.unauthorized => BleAdapterState.unauthorized,
|
||||||
|
fbp.BluetoothAdapterState.unavailable => BleAdapterState.unsupported,
|
||||||
|
fbp.BluetoothAdapterState.unknown => BleAdapterState.unknown,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> turnOn() async {
|
||||||
|
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
_log('ADAPTER turnOn requested');
|
||||||
|
await fbp.FlutterBluePlus.turnOn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<List<ScooterDevice>> scan() {
|
Stream<List<ScooterDevice>> scan() {
|
||||||
late StreamController<List<ScooterDevice>> controller;
|
late StreamController<List<ScooterDevice>> controller;
|
||||||
@@ -70,12 +100,7 @@ class FlutterBleClient extends BleClient {
|
|||||||
|
|
||||||
Future<void> start() async {
|
Future<void> start() async {
|
||||||
try {
|
try {
|
||||||
var adapter = await fbp.FlutterBluePlus.adapterState.first;
|
await fbp.FlutterBluePlus.adapterState
|
||||||
if (adapter != fbp.BluetoothAdapterState.on &&
|
|
||||||
defaultTargetPlatform == TargetPlatform.android) {
|
|
||||||
await fbp.FlutterBluePlus.turnOn();
|
|
||||||
}
|
|
||||||
adapter = await fbp.FlutterBluePlus.adapterState
|
|
||||||
.where((s) => s == fbp.BluetoothAdapterState.on)
|
.where((s) => s == fbp.BluetoothAdapterState.on)
|
||||||
.first
|
.first
|
||||||
.timeout(const Duration(seconds: 10));
|
.timeout(const Duration(seconds: 10));
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
/// Outcome of a Bluetooth permission check or request.
|
||||||
|
enum BleAccess {
|
||||||
|
/// Everything granted. Bluetooth adapter state is checked separately.
|
||||||
|
granted,
|
||||||
|
|
||||||
|
/// The user declined but can be asked again.
|
||||||
|
denied,
|
||||||
|
|
||||||
|
/// The user declined with "don't ask again" (Android) or in Settings (iOS).
|
||||||
|
/// Only the system settings screen can fix this.
|
||||||
|
permanentlyDenied,
|
||||||
|
|
||||||
|
/// The platform reported Bluetooth as unavailable.
|
||||||
|
unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requests the runtime permissions OpenMotion needs to scan and connect.
|
||||||
|
///
|
||||||
|
/// Android 12+ (API 31): BLUETOOTH_SCAN and BLUETOOTH_CONNECT, shown to the
|
||||||
|
/// user as "Nearby devices". Android 11 and below: legacy BLE scanning needs
|
||||||
|
/// location while in use. iOS: the single Bluetooth permission, which the OS
|
||||||
|
/// prompts for the first time CoreBluetooth is touched.
|
||||||
|
class BlePermissions {
|
||||||
|
BlePermissions._();
|
||||||
|
|
||||||
|
static int? _androidSdk;
|
||||||
|
|
||||||
|
static Future<int> androidSdk() async {
|
||||||
|
if (!Platform.isAndroid) return 0;
|
||||||
|
return _androidSdk ??= (await DeviceInfoPlugin().androidInfo).version.sdkInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Permission>> _needed() async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final sdk = await androidSdk();
|
||||||
|
return sdk >= 31
|
||||||
|
? const [Permission.bluetoothScan, Permission.bluetoothConnect]
|
||||||
|
: const [Permission.locationWhenInUse];
|
||||||
|
}
|
||||||
|
if (Platform.isIOS || Platform.isMacOS) return const [Permission.bluetooth];
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks without prompting.
|
||||||
|
static Future<BleAccess> status() async {
|
||||||
|
final statuses = <PermissionStatus>[];
|
||||||
|
for (final p in await _needed()) {
|
||||||
|
statuses.add(await p.status);
|
||||||
|
}
|
||||||
|
return _combine(statuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prompts for anything not yet granted.
|
||||||
|
static Future<BleAccess> request() async {
|
||||||
|
final needed = await _needed();
|
||||||
|
if (needed.isEmpty) return BleAccess.granted;
|
||||||
|
final results = await needed.request();
|
||||||
|
return _combine(results.values.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
static BleAccess _combine(List<PermissionStatus> statuses) {
|
||||||
|
if (statuses.any((s) => s.isPermanentlyDenied || s.isRestricted)) {
|
||||||
|
return BleAccess.permanentlyDenied;
|
||||||
|
}
|
||||||
|
if (statuses.any((s) => s.isDenied)) return BleAccess.denied;
|
||||||
|
return BleAccess.granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> openSettings() => openAppSettings();
|
||||||
|
}
|
||||||
@@ -89,6 +89,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.15"
|
version: "0.7.15"
|
||||||
|
device_info_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: device_info_plus
|
||||||
|
sha256: "0891702f96b2e465fe567b7ec448380e6b1c14f60af552a8536d9f583b6b8442"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.2.0"
|
||||||
|
device_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_platform_interface
|
||||||
|
sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.1.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -416,6 +432,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.3"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.0.1"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.6.1"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.4+1"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: ed86a61c190258fdd65de395ea0632822e3415c1faec38eae0c31b479c28a531
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.4.1"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -605,6 +669,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.4.0"
|
version: "6.4.0"
|
||||||
|
win32_registry:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32_registry
|
||||||
|
sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ dependencies:
|
|||||||
path_provider: ^2.1.6
|
path_provider: ^2.1.6
|
||||||
flutter_secure_storage: ^11.2.0
|
flutter_secure_storage: ^11.2.0
|
||||||
shared_preferences: ^2.5.5
|
shared_preferences: ^2.5.5
|
||||||
|
permission_handler: ^12.0.0
|
||||||
|
device_info_plus: ^13.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user