From e661171fd212d69dc147e1cab3657adf83a71dd5 Mon Sep 17 00:00:00 2001 From: Varakh Date: Sun, 26 Nov 2023 23:52:52 +0100 Subject: [PATCH] Various improvements and #noissue - Bumped Android minSdk to 30 (Android 11) - Fixed permission service not handling Android SDK 33 correctly - Fixed permission service not being started during application start --- CHANGELOG.md | 5 +- android/app/build.gradle | 2 +- android/app/src/debug/AndroidManifest.xml | 5 +- android/app/src/main/AndroidManifest.xml | 5 +- android/app/src/profile/AndroidManifest.xml | 5 +- assets/i18n/en.json | 8 -- lib/core/services/permission_service.dart | 147 +++++++++++--------- lib/core/services/storage_service.dart | 10 -- lib/core/viewmodels/startup_model.dart | 7 +- pubspec.lock | 24 ++++ pubspec.yaml | 1 + 11 files changed, 128 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34c9438..8358f70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ## 1.6.2+20 - UNRELEASED * Updated internal dependencies -* Move progress indicator of _Show Configuration_ into button +* Moved progress indicator of _Show Configuration_ into the underlying button +* Bumped Android minSdk to `30` (Android 11) +* Fixed permission service not handling Android SDK 33 correctly +* Fixed permission service not being started during application start ## 1.6.1+19 * Updated internal dependencies diff --git a/android/app/build.gradle b/android/app/build.gradle index 070d44a..79db66a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -39,7 +39,7 @@ android { defaultConfig { applicationId "de.varakh.fbmobile" - minSdkVersion 19 + minSdkVersion 30 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 5399da6..9a1fc0d 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -4,7 +4,10 @@ to allow setting breakpoints, to provide hot reload, etc. --> - + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2c6fc11..427ec05 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,7 +30,10 @@ - + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index 5399da6..9a1fc0d 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -4,7 +4,10 @@ to allow setting breakpoints, to provide hot reload, etc. --> - + + + + diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 1d5cea4..bbfd3a8 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -126,14 +126,6 @@ "description": "Could not open '{link}'. Please ensure that you have an application installed which handles opening such link types." } }, - "permission_service": { - "dialog": { - "title": "Storage permission", - "description": "Storage permission should be granted to the app so that it can work properly. Do you want to grant permission or ignore this message permanently in the future?", - "grant": "Grant", - "ignore": "Ignore" - } - }, "dialog": { "confirm": "OK", "cancel": "Cancel" diff --git a/lib/core/services/permission_service.dart b/lib/core/services/permission_service.dart index 4c5dcdc..ad091f8 100644 --- a/lib/core/services/permission_service.dart +++ b/lib/core/services/permission_service.dart @@ -1,108 +1,100 @@ import 'dart:async'; +import 'dart:io' show Platform; -import 'package:flutter_translate/flutter_translate.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:logger/logger.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../constants.dart'; -import '../../core/datamodels/dialog_response.dart'; -import '../../core/services/dialog_service.dart'; import '../../core/services/stoppable_service.dart'; import '../../core/util/logger.dart'; -import '../../locator.dart'; -import 'storage_service.dart'; class PermissionService extends StoppableService { final Logger _logger = getLogger(); - final DialogService _dialogService = locator(); - final StorageService _storageService = locator(); Timer? _serviceCheckTimer; - PermissionStatus? _permissionStatus; - - bool _permanentlyIgnored = false; bool _devicePermissionDialogActive = false; - bool _ownPermissionDialogActive = false; - PermissionService() { - _devicePermissionDialogActive = true; + bool _deviceInformationInitialized = false; + bool _useStoragePermission = true; - Permission.storage.request().then((status) { - _permissionStatus = status; - if (PermissionStatus.permanentlyDenied == status) { - _permanentlyIgnored = true; - } - }).whenComplete(() { - _logger.d('Initial device request permission finished'); - _devicePermissionDialogActive = false; - }); - } + PermissionService(); Future checkEnabledAndPermission() async { - if (_permanentlyIgnored) { - await _storageService.storeStoragePermissionDialogIgnored(); - _permanentlyIgnored = false; - _logger.d('Set permanently ignored permission request'); - stop(); - } - if (_devicePermissionDialogActive) { _logger.d('Device permission dialog active, skipping'); return; } - if (_ownPermissionDialogActive) { - _logger.d('Own permission dialog already active, skipping'); - return; + bool allGranted = false; + bool anyPermanentlyDenied = false; + + // Since Android compileSdk >= 33, "storage" is deprecated + // Instead, request access to all of + // - Permission.photos + // - Permission.videos + // - Permission.audio + // + // For iOS and Android < 33, keep using "storage" + if (_useStoragePermission) { + PermissionStatus storagePermission = await Permission.storage.status; + allGranted = PermissionStatus.granted == storagePermission; + anyPermanentlyDenied = + PermissionStatus.permanentlyDenied == storagePermission; + } else { + PermissionStatus photosPermission = await Permission.photos.status; + PermissionStatus videosPermission = await Permission.videos.status; + PermissionStatus audioPermission = await Permission.audio.status; + + allGranted = PermissionStatus.granted == photosPermission && + PermissionStatus.granted == videosPermission && + PermissionStatus.granted == audioPermission; + anyPermanentlyDenied = + PermissionStatus.permanentlyDenied == photosPermission || + PermissionStatus.permanentlyDenied == videosPermission || + PermissionStatus.permanentlyDenied == audioPermission; } - var ignoredDialog = - await _storageService.hasStoragePermissionDialogIgnored(); - - if (ignoredDialog) { - _logger.d('Permanently ignored permission request, skipping'); + // show warning to user to manually handle, don't enforce it over and over again + if (anyPermanentlyDenied) { + _logger.w( + "At least one required permission has been denied permanently, stopping service"); stop(); return; } - _permissionStatus = await Permission.storage.status; - if (_permissionStatus != PermissionStatus.granted) { - if (_permissionStatus == PermissionStatus.permanentlyDenied) { - await _storageService.storeStoragePermissionDialogIgnored(); - return; - } + // all good, stop the permission service + if (allGranted) { + _logger.d("All permissions have been granted, stopping service"); + stop(); + return; + } - _ownPermissionDialogActive = true; - DialogResponse response = await _dialogService.showConfirmationDialog( - title: translate('permission_service.dialog.title'), - description: translate('permission_service.dialog.description'), - buttonTitleAccept: translate('permission_service.dialog.grant'), - buttonTitleDeny: translate('permission_service.dialog.ignore')); + // not all have been granted, show OS dialog + _logger.d( + "Not all permissions have been granted yet, initializing permission dialog"); + _devicePermissionDialogActive = true; - if (!response.confirmed!) { - await _storageService.storeStoragePermissionDialogIgnored(); - } else { - _devicePermissionDialogActive = true; - Permission.storage.request().then((status) async { - if (PermissionStatus.permanentlyDenied == status) { - await _storageService.storeStoragePermissionDialogIgnored(); - } - }).whenComplete(() { - _logger.d('Device request permission finished'); - _devicePermissionDialogActive = false; - }); - } - - _ownPermissionDialogActive = false; + if (_useStoragePermission) { + await [Permission.storage].request().whenComplete(() { + _logger.d('Device request permission finished'); + _devicePermissionDialogActive = false; + }); } else { - await _storageService.storeStoragePermissionDialogIgnored(); + await [Permission.photos, Permission.videos, Permission.audio] + .request() + .whenComplete(() { + _logger.d('Device request permission finished'); + _devicePermissionDialogActive = false; + }); } } @override Future start() async { super.start(); + await _determineDeviceInfo(); await checkEnabledAndPermission(); _serviceCheckTimer = Timer.periodic( @@ -124,6 +116,29 @@ class PermissionService extends StoppableService { _logger.d('PermissionService stopped'); } + Future _determineDeviceInfo() async { + if (_deviceInformationInitialized) { + _logger.d('Device information already initialized, skipping'); + return; + } + + DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + if (Platform.isAndroid) { + final androidInfo = await deviceInfoPlugin.androidInfo; + if (androidInfo.version.sdkInt >= 33) { + _useStoragePermission = false; + } + } + + if (_useStoragePermission) { + _logger.d('Device requires [storage] permission'); + } else { + _logger.d('Device requires [photos,videos,audio] permission'); + } + + _deviceInformationInitialized = true; + } + void _removeServiceCheckTimer() { if (_serviceCheckTimer != null) { _serviceCheckTimer!.cancel(); diff --git a/lib/core/services/storage_service.dart b/lib/core/services/storage_service.dart index 95f08c4..ba3ce5c 100644 --- a/lib/core/services/storage_service.dart +++ b/lib/core/services/storage_service.dart @@ -7,8 +7,6 @@ import '../models/session.dart'; class StorageService { static const _sessionKey = 'session'; static const _lastUrlKey = 'last_url'; - static const _storagePermissionDialogIgnoredKey = - 'storage_permission_ignored'; Future storeLastUrl(String url) { return _store(_lastUrlKey, url); @@ -39,14 +37,6 @@ class StorageService { return _remove(_sessionKey); } - Future storeStoragePermissionDialogIgnored() { - return _store(_storagePermissionDialogIgnoredKey, true.toString()); - } - - Future hasStoragePermissionDialogIgnored() { - return _exists(_storagePermissionDialogIgnoredKey); - } - Future _exists(String key) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.containsKey(key); diff --git a/lib/core/viewmodels/startup_model.dart b/lib/core/viewmodels/startup_model.dart index 767e1cb..afc8eac 100644 --- a/lib/core/viewmodels/startup_model.dart +++ b/lib/core/viewmodels/startup_model.dart @@ -1,3 +1,4 @@ +import 'package:fbmobile/core/services/permission_service.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../locator.dart'; @@ -9,16 +10,18 @@ import 'base_model.dart'; class StartUpViewModel extends BaseModel { final SessionService _sessionService = locator(); + final PermissionService _permissionService = locator(); final NavigationService _navigationService = locator(); Future handleStartUpLogic() async { setStateView(ViewState.busy); setStateMessage(translate('startup.init')); - await Future.delayed(const Duration(milliseconds: 150)); + await Future.delayed(const Duration(milliseconds: 100)); setStateMessage(translate('startup.start_services')); await _sessionService.start(); - await Future.delayed(const Duration(milliseconds: 150)); + await _permissionService.start(); + await Future.delayed(const Duration(milliseconds: 100)); _navigationService.navigateAndReplaceTo(HomeView.routeName); diff --git a/pubspec.lock b/pubspec.lock index 0f4f523..1c1344c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,6 +201,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.4" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + url: "https://pub.dev" + source: hosted + version: "9.1.1" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" dynamic_color: dependency: "direct main" description: @@ -994,6 +1010,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ab73f76..48ac2c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: intl: 0.18.1 path: 1.8.3 flutter_sharing_intent: 1.1.0 + device_info_plus: 9.1.1 dev_dependencies: flutter_test: