From 1e58ae94b9bb79131f00c472a9bdccdef33072b6 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 30 Sep 2024 23:34:46 +0200 Subject: [PATCH] enterprise: user/work profile switch from drawer --- CHANGELOG.md | 1 + android/app/src/main/AndroidManifest.xml | 4 + .../deckers/thibault/aves/MainActivity.kt | 5 +- .../aves/channel/calls/AppAdapterHandler.kt | 9 +- .../aves/channel/calls/AppProfileHandler.kt | 97 ++++++++++++ lib/services/app_profile_service.dart | 89 +++++++++++ lib/services/common/services.dart | 3 + lib/services/storage_service.dart | 1 - lib/widgets/debug/app_debug_page.dart | 20 +-- lib/widgets/debug/capabilities.dart | 133 +++++++++++++++++ lib/widgets/debug/device.dart | 48 ------ .../debug/{android_apps.dart => os_apps.dart} | 10 +- .../{android_codecs.dart => os_codecs.dart} | 10 +- .../{android_dirs.dart => os_paths.dart} | 10 +- .../debug/{storage.dart => os_storage.dart} | 10 +- lib/widgets/navigation/drawer/app_drawer.dart | 141 +++++++++++++----- 16 files changed, 470 insertions(+), 121 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppProfileHandler.kt create mode 100644 lib/services/app_profile_service.dart create mode 100644 lib/widgets/debug/capabilities.dart delete mode 100644 lib/widgets/debug/device.dart rename lib/widgets/debug/{android_apps.dart => os_apps.dart} (93%) rename lib/widgets/debug/{android_codecs.dart => os_codecs.dart} (90%) rename lib/widgets/debug/{android_dirs.dart => os_paths.dart} (78%) rename lib/widgets/debug/{storage.dart => os_storage.dart} (84%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a375b5a78..9b058d5c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Added - Map: OpenTopoMap layer +- Enterprise: support for work profile switching from the drawer ## [v1.11.13] - 2024-09-17 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 229782b9d..b04dc8e34 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -39,6 +39,10 @@ + + diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt index 1d9844578..b3a9fd835 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -1,6 +1,7 @@ package deckers.thibault.aves import android.annotation.SuppressLint +import android.app.KeyguardManager import android.app.SearchManager import android.appwidget.AppWidgetManager import android.content.ClipData @@ -23,6 +24,7 @@ import deckers.thibault.aves.channel.AvesByteSendingMethodCodec import deckers.thibault.aves.channel.calls.AccessibilityHandler import deckers.thibault.aves.channel.calls.AnalysisHandler import deckers.thibault.aves.channel.calls.AppAdapterHandler +import deckers.thibault.aves.channel.calls.AppProfileHandler import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.DebugHandler import deckers.thibault.aves.channel.calls.DeviceHandler @@ -142,6 +144,7 @@ open class MainActivity : FlutterFragmentActivity() { MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this)) MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this)) // - need Activity + MethodChannel(messenger, AppProfileHandler.CHANNEL).setMethodCallHandler(AppProfileHandler(this)) MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this)) // result streaming: dart -> platform ->->-> dart @@ -318,7 +321,7 @@ open class MainActivity : FlutterFragmentActivity() { INTENT_DATA_KEY_URI to uri.toString(), ) - val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as android.app.KeyguardManager + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager val isLocked = keyguardManager.isKeyguardLocked if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setShowWhenLocked(isLocked) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 1abc8cb32..65c3f38a3 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -1,6 +1,10 @@ package deckers.thibault.aves.channel.calls -import android.content.* +import android.content.ClipData +import android.content.ClipboardManager +import android.content.ContentResolver +import android.content.Context +import android.content.Intent import android.content.pm.ApplicationInfo import android.content.res.Configuration import android.content.res.Resources @@ -42,7 +46,8 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File -import java.util.* +import java.util.Locale +import java.util.UUID import kotlin.math.roundToInt class AppAdapterHandler(private val context: Context) : MethodCallHandler { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppProfileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppProfileHandler.kt new file mode 100644 index 000000000..57003e521 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppProfileHandler.kt @@ -0,0 +1,97 @@ +package deckers.thibault.aves.channel.calls + +import android.app.Activity +import android.content.Context +import android.content.pm.CrossProfileApps +import android.os.Build +import deckers.thibault.aves.channel.calls.Coresult.Companion.safe +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler + +class AppProfileHandler(private val activity: Activity) : MethodCallHandler { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "canInteractAcrossProfiles" -> safe(call, result, ::canInteractAcrossProfiles) + "canRequestInteractAcrossProfiles" -> safe(call, result, ::canRequestInteractAcrossProfiles) + "requestInteractAcrossProfiles" -> safe(call, result, ::requestInteractAcrossProfiles) + "switchProfile" -> safe(call, result, ::switchProfile) + "getProfileSwitchingLabel" -> safe(call, result, ::getProfileSwitchingLabel) + "getTargetUserProfiles" -> safe(call, result, ::getTargetUserProfiles) + else -> result.notImplemented() + } + } + + private fun canInteractAcrossProfiles(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + result.success(false) + return + } + + val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps + result.success(crossProfileApps.canInteractAcrossProfiles()) + } + + private fun canRequestInteractAcrossProfiles(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + result.success(false) + return + } + + val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps + result.success(crossProfileApps.canRequestInteractAcrossProfiles()) + } + + private fun requestInteractAcrossProfiles(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + result.success(false) + return + } + + val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps + val intent = crossProfileApps.createRequestInteractAcrossProfilesIntent() + val started = activity.startActivity(intent) + + result.success(started) + } + + private fun switchProfile(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + result.success(false) + return + } + + val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps + val userHandles = crossProfileApps.targetUserProfiles + crossProfileApps.startMainActivity(activity.componentName, userHandles.first()) + result.success(null) + } + + private fun getProfileSwitchingLabel(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + result.success(null) + return + } + + val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps + val userHandles = crossProfileApps.targetUserProfiles + val label = crossProfileApps.getProfileSwitchingLabel(userHandles.first()) + + result.success(label) + } + + private fun getTargetUserProfiles(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + result.success(false) + return + } + + val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps + val userProfiles = crossProfileApps.targetUserProfiles.map { it.toString() }.toList() + result.success(userProfiles) + } + + companion object { + const val CHANNEL = "deckers.thibault/aves/app_profile" + } +} diff --git a/lib/services/app_profile_service.dart b/lib/services/app_profile_service.dart new file mode 100644 index 000000000..c3b9fe711 --- /dev/null +++ b/lib/services/app_profile_service.dart @@ -0,0 +1,89 @@ +import 'dart:async'; + +import 'package:aves/services/common/services.dart'; +import 'package:flutter/services.dart'; + +abstract class AppProfileService { + Future canInteractAcrossProfiles(); + + Future canRequestInteractAcrossProfiles(); + + Future requestInteractAcrossProfiles(); + + Future getProfileSwitchingLabel(); + + Future switchProfile(); + + Future> getTargetUserProfiles(); +} + +class PlatformAppProfileService implements AppProfileService { + static const _platform = MethodChannel('deckers.thibault/aves/app_profile'); + + @override + Future canInteractAcrossProfiles() async { + try { + final result = await _platform.invokeMethod('canInteractAcrossProfiles'); + if (result != null) return result as bool; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return false; + } + + @override + Future canRequestInteractAcrossProfiles() async { + try { + final result = await _platform.invokeMethod('canRequestInteractAcrossProfiles'); + if (result != null) return result as bool; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return false; + } + + @override + Future requestInteractAcrossProfiles() async { + try { + final result = await _platform.invokeMethod('requestInteractAcrossProfiles'); + if (result != null) return result as bool; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return false; + } + + @override + Future switchProfile() async { + try { + await _platform.invokeMethod('switchProfile'); + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return; + } + + @override + Future getProfileSwitchingLabel() async { + try { + final result = await _platform.invokeMethod('getProfileSwitchingLabel'); + return result as String; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return ''; + } + + @override + Future> getTargetUserProfiles() async { + try { + final result = await _platform.invokeMethod('getTargetUserProfiles'); + if (result != null) { + return (result as List).cast(); + } + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return []; + } +} diff --git a/lib/services/common/services.dart b/lib/services/common/services.dart index e66851f7d..465f2b471 100644 --- a/lib/services/common/services.dart +++ b/lib/services/common/services.dart @@ -2,6 +2,7 @@ import 'package:aves/model/availability.dart'; import 'package:aves/model/db/db.dart'; import 'package:aves/model/db/db_sqflite.dart'; import 'package:aves/model/settings/store_shared_pref.dart'; +import 'package:aves/services/app_profile_service.dart'; import 'package:aves/services/app_service.dart'; import 'package:aves/services/device_service.dart'; import 'package:aves/services/media/embedded_data_service.dart'; @@ -37,6 +38,7 @@ final AvesVideoControllerFactory videoControllerFactory = getIt(); final AppService appService = getIt(); +final AppProfileService appProfileService = getIt(); final DeviceService deviceService = getIt(); final EmbeddedDataService embeddedDataService = getIt(); final MediaEditService mediaEditService = getIt(); @@ -59,6 +61,7 @@ void initPlatformServices() { getIt.registerLazySingleton(FfmpegVideoMetadataFetcher.new); getIt.registerLazySingleton(PlatformAppService.new); + getIt.registerLazySingleton(PlatformAppProfileService.new); getIt.registerLazySingleton(PlatformDeviceService.new); getIt.registerLazySingleton(PlatformEmbeddedDataService.new); getIt.registerLazySingleton(PlatformMediaEditService.new); diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 78ac5cf97..765f87200 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -176,7 +176,6 @@ class PlatformStorageService implements StorageService { } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } - return; } // returns number of deleted directories diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 79e7259a1..004d4193f 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -12,19 +12,19 @@ import 'package:aves/widgets/common/basic/popup/menu_row.dart'; import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; -import 'package:aves/widgets/debug/android_apps.dart'; -import 'package:aves/widgets/debug/android_codecs.dart'; -import 'package:aves/widgets/debug/android_dirs.dart'; import 'package:aves/widgets/debug/app_debug_action.dart'; import 'package:aves/widgets/debug/cache.dart'; +import 'package:aves/widgets/debug/capabilities.dart'; import 'package:aves/widgets/debug/colors.dart'; import 'package:aves/widgets/debug/database.dart'; -import 'package:aves/widgets/debug/device.dart'; import 'package:aves/widgets/debug/general.dart'; import 'package:aves/widgets/debug/media_store_scan_dialog.dart'; +import 'package:aves/widgets/debug/os_apps.dart'; +import 'package:aves/widgets/debug/os_codecs.dart'; +import 'package:aves/widgets/debug/os_paths.dart'; +import 'package:aves/widgets/debug/os_storage.dart'; import 'package:aves/widgets/debug/report.dart'; import 'package:aves/widgets/debug/settings.dart'; -import 'package:aves/widgets/debug/storage.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -73,16 +73,16 @@ class AppDebugPage extends StatelessWidget { padding: const EdgeInsets.all(8), children: const [ DebugGeneralSection(), - DebugAndroidAppSection(), - DebugAndroidCodecSection(), - DebugAndroidDirSection(), DebugCacheSection(), + DebugCapabilitiesSection(), DebugColorSection(), DebugAppDatabaseSection(), - DebugDeviceSection(), DebugErrorReportingSection(), DebugSettingsSection(), - DebugStorageSection(), + DebugOSAppSection(), + DebugOSCodecSection(), + DebugOSPathSection(), + DebugOSStorageSection(), ], ), ), diff --git a/lib/widgets/debug/capabilities.dart b/lib/widgets/debug/capabilities.dart new file mode 100644 index 000000000..d8d7c9261 --- /dev/null +++ b/lib/widgets/debug/capabilities.dart @@ -0,0 +1,133 @@ +import 'package:aves/model/device.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/identity/highlight_title.dart'; +import 'package:aves/widgets/viewer/info/common.dart'; +import 'package:flutter/material.dart'; + +class DebugCapabilitiesSection extends StatefulWidget { + const DebugCapabilitiesSection({super.key}); + + @override + State createState() => _DebugCapabilitiesSectionState(); +} + +class _DebugCapabilitiesSectionState extends State with AutomaticKeepAliveClientMixin { + late final Future> _appFuture, _windowFuture; + + @override + void initState() { + super.initState(); + _appFuture = Future.wait([ + appProfileService.canInteractAcrossProfiles(), + appProfileService.canRequestInteractAcrossProfiles(), + appProfileService.getTargetUserProfiles(), + ]); + _windowFuture = Future.wait([ + windowService.isCutoutAware(), + windowService.isRotationLocked(), + windowService.supportsHdr(), + ]); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return AvesExpansionTile( + title: 'Capabilities', + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const HighlightTitle(title: 'Device'), + InfoRowGroup( + info: { + 'canAuthenticateUser': '${device.canAuthenticateUser}', + 'canPinShortcut': '${device.canPinShortcut}', + 'canRenderFlagEmojis': '${device.canRenderFlagEmojis}', + 'canRenderSubdivisionFlagEmojis': '${device.canRenderSubdivisionFlagEmojis}', + 'canRequestManageMedia': '${device.canRequestManageMedia}', + 'canSetLockScreenWallpaper': '${device.canSetLockScreenWallpaper}', + 'hasGeocoder': '${device.hasGeocoder}', + 'isDynamicColorAvailable': '${device.isDynamicColorAvailable}', + 'isTelevision': '${device.isTelevision}', + 'showPinShortcutFeedback': '${device.showPinShortcutFeedback}', + 'supportEdgeToEdgeUIMode': '${device.supportEdgeToEdgeUIMode}', + 'supportPictureInPicture': '${device.supportPictureInPicture}', + }, + ), + ], + ), + ), + FutureBuilder>( + future: _windowFuture, + builder: (context, snapshot) { + final data = snapshot.data; + if (data == null) { + return const SizedBox(); + } + final [ + bool isCutoutAware, + bool isRotationLocked, + bool supportsHdr, + ] = data; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const HighlightTitle(title: 'Window'), + InfoRowGroup( + info: { + 'isCutoutAware': '$isCutoutAware', + 'isRotationLocked': '$isRotationLocked', + 'supportsHdr': '$supportsHdr', + }, + ), + ], + ), + ); + }, + ), + FutureBuilder>( + future: _appFuture, + builder: (context, snapshot) { + final data = snapshot.data; + if (data == null) { + return const SizedBox(); + } + final [ + bool canInteractAcrossProfiles, + bool canRequestInteractAcrossProfiles, + List targetUserProfiles, + ] = data; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const HighlightTitle(title: 'App'), + InfoRowGroup( + info: { + 'userAgent': device.userAgent, + 'canInteractAcrossProfiles': '$canInteractAcrossProfiles', + 'canRequestInteractAcrossProfiles': '$canRequestInteractAcrossProfiles', + 'targetUserProfiles': '$targetUserProfiles', + }, + ), + ], + ), + ); + }, + ), + ], + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/debug/device.dart b/lib/widgets/debug/device.dart deleted file mode 100644 index 6a758660a..000000000 --- a/lib/widgets/debug/device.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:aves/model/device.dart'; -import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; -import 'package:aves/widgets/viewer/info/common.dart'; -import 'package:flutter/material.dart'; - -class DebugDeviceSection extends StatefulWidget { - const DebugDeviceSection({super.key}); - - @override - State createState() => _DebugDeviceSectionState(); -} - -class _DebugDeviceSectionState extends State with AutomaticKeepAliveClientMixin { - @override - Widget build(BuildContext context) { - super.build(context); - return AvesExpansionTile( - title: 'Device', - children: [ - Padding( - padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: InfoRowGroup( - info: { - 'packageName': device.packageName, - 'packageVersion': device.packageVersion, - 'userAgent': device.userAgent, - 'canAuthenticateUser': '${device.canAuthenticateUser}', - 'canPinShortcut': '${device.canPinShortcut}', - 'canRenderFlagEmojis': '${device.canRenderFlagEmojis}', - 'canRenderSubdivisionFlagEmojis': '${device.canRenderSubdivisionFlagEmojis}', - 'canRequestManageMedia': '${device.canRequestManageMedia}', - 'canSetLockScreenWallpaper': '${device.canSetLockScreenWallpaper}', - 'hasGeocoder': '${device.hasGeocoder}', - 'isDynamicColorAvailable': '${device.isDynamicColorAvailable}', - 'isTelevision': '${device.isTelevision}', - 'showPinShortcutFeedback': '${device.showPinShortcutFeedback}', - 'supportEdgeToEdgeUIMode': '${device.supportEdgeToEdgeUIMode}', - 'supportPictureInPicture': '${device.supportPictureInPicture}', - }, - ), - ), - ], - ); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/lib/widgets/debug/android_apps.dart b/lib/widgets/debug/os_apps.dart similarity index 93% rename from lib/widgets/debug/android_apps.dart rename to lib/widgets/debug/os_apps.dart index 101b18c08..d8250a4d8 100644 --- a/lib/widgets/debug/android_apps.dart +++ b/lib/widgets/debug/os_apps.dart @@ -7,14 +7,14 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -class DebugAndroidAppSection extends StatefulWidget { - const DebugAndroidAppSection({super.key}); +class DebugOSAppSection extends StatefulWidget { + const DebugOSAppSection({super.key}); @override - State createState() => _DebugAndroidAppSectionState(); + State createState() => _DebugOSAppSectionState(); } -class _DebugAndroidAppSectionState extends State with AutomaticKeepAliveClientMixin { +class _DebugOSAppSectionState extends State with AutomaticKeepAliveClientMixin { late Future> _loader; final ValueNotifier _queryNotifier = ValueNotifier(''); @@ -37,7 +37,7 @@ class _DebugAndroidAppSectionState extends State with Au super.build(context); return AvesExpansionTile( - title: 'Android Apps', + title: 'OS Apps', children: [ Padding( padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), diff --git a/lib/widgets/debug/android_codecs.dart b/lib/widgets/debug/os_codecs.dart similarity index 90% rename from lib/widgets/debug/android_codecs.dart rename to lib/widgets/debug/os_codecs.dart index dfdfe169a..e1a4cf99f 100644 --- a/lib/widgets/debug/android_codecs.dart +++ b/lib/widgets/debug/os_codecs.dart @@ -6,14 +6,14 @@ import 'package:aves/widgets/viewer/info/common.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -class DebugAndroidCodecSection extends StatefulWidget { - const DebugAndroidCodecSection({super.key}); +class DebugOSCodecSection extends StatefulWidget { + const DebugOSCodecSection({super.key}); @override - State createState() => _DebugAndroidCodecSectionState(); + State createState() => _DebugOSCodecSectionState(); } -class _DebugAndroidCodecSectionState extends State with AutomaticKeepAliveClientMixin { +class _DebugOSCodecSectionState extends State with AutomaticKeepAliveClientMixin { late Future> _loader; final ValueNotifier _queryNotifier = ValueNotifier(''); @@ -34,7 +34,7 @@ class _DebugAndroidCodecSectionState extends State wit super.build(context); return AvesExpansionTile( - title: 'Android Codecs', + title: 'OS Codecs', children: [ Padding( padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), diff --git a/lib/widgets/debug/android_dirs.dart b/lib/widgets/debug/os_paths.dart similarity index 78% rename from lib/widgets/debug/android_dirs.dart rename to lib/widgets/debug/os_paths.dart index d4abc67ae..1b1734a08 100644 --- a/lib/widgets/debug/android_dirs.dart +++ b/lib/widgets/debug/os_paths.dart @@ -5,14 +5,14 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; -class DebugAndroidDirSection extends StatefulWidget { - const DebugAndroidDirSection({super.key}); +class DebugOSPathSection extends StatefulWidget { + const DebugOSPathSection({super.key}); @override - State createState() => _DebugAndroidDirSectionState(); + State createState() => _DebugOSPathSectionState(); } -class _DebugAndroidDirSectionState extends State with AutomaticKeepAliveClientMixin { +class _DebugOSPathSectionState extends State with AutomaticKeepAliveClientMixin { late Future _loader; @override @@ -26,7 +26,7 @@ class _DebugAndroidDirSectionState extends State with Au super.build(context); return AvesExpansionTile( - title: 'Android Dirs', + title: 'OS Paths', children: [ Padding( padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), diff --git a/lib/widgets/debug/storage.dart b/lib/widgets/debug/os_storage.dart similarity index 84% rename from lib/widgets/debug/storage.dart rename to lib/widgets/debug/os_storage.dart index 4b0c49e77..5765e513c 100644 --- a/lib/widgets/debug/storage.dart +++ b/lib/widgets/debug/os_storage.dart @@ -7,14 +7,14 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; -class DebugStorageSection extends StatefulWidget { - const DebugStorageSection({super.key}); +class DebugOSStorageSection extends StatefulWidget { + const DebugOSStorageSection({super.key}); @override - State createState() => _DebugStorageSectionState(); + State createState() => _DebugOSStorageSectionState(); } -class _DebugStorageSectionState extends State with AutomaticKeepAliveClientMixin { +class _DebugOSStorageSectionState extends State with AutomaticKeepAliveClientMixin { final Map _freeSpaceByVolume = {}; @override @@ -31,7 +31,7 @@ class _DebugStorageSectionState extends State with Automati super.build(context); return AvesExpansionTile( - title: 'Storage Volumes', + title: 'OS Storage', children: [ ...androidFileUtils.storageVolumes.expand((v) { final freeSpace = _freeSpaceByVolume[v.path]; diff --git a/lib/widgets/navigation/drawer/app_drawer.dart b/lib/widgets/navigation/drawer/app_drawer.dart index 3bc6ad387..b00591e6d 100644 --- a/lib/widgets/navigation/drawer/app_drawer.dart +++ b/lib/widgets/navigation/drawer/app_drawer.dart @@ -8,6 +8,7 @@ import 'package:aves/model/source/location/country.dart'; import 'package:aves/model/source/location/place.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/ref/locales.dart'; +import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; @@ -56,13 +57,50 @@ class AppDrawer extends StatefulWidget { } } -class _AppDrawerState extends State { +class _AppDrawerState extends State with WidgetsBindingObserver { // using the default controller conflicts // with bottom nav bar primary scroll monitoring final ScrollController _scrollController = ScrollController(); + late Future> _profileSwitchFuture; + bool _profileSwitchPermissionRequested = false; CollectionLens? get currentCollection => widget.currentCollection; + @override + void initState() { + super.initState(); + _initProfileSwitchFuture(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.resumed: + if (_profileSwitchPermissionRequested) { + _profileSwitchPermissionRequested = false; + _initProfileSwitchFuture(); + setState(() {}); + } + default: + break; + } + } + + void _initProfileSwitchFuture() { + _profileSwitchFuture = Future.wait([ + appProfileService.canRequestInteractAcrossProfiles(), + appProfileService.canInteractAcrossProfiles(), + appProfileService.getProfileSwitchingLabel(), + ]); + } + @override Widget build(BuildContext context) { final drawerItems = [ @@ -133,44 +171,44 @@ class _AppDrawerState extends State { color: colorScheme.primary, child: SafeArea( bottom: false, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 6), - Align( - alignment: AlignmentDirectional.centerStart, - child: Wrap( - spacing: 16, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - const AvesLogo(size: 48), - OutlinedText( - textSpans: [ - TextSpan( - text: l10n.appName, - style: TextStyle( - color: Colors.white, - fontSize: 38, - fontWeight: FontWeight.w300, - letterSpacing: canHaveLetterSpacing(context.locale) ? 1 : 0, - fontFeatures: const [FontFeature.enable('smcp')], - ), - ), - ], - ), - ], - ), + child: OutlinedButtonTheme( + data: OutlinedButtonThemeData( + style: ButtonStyle( + foregroundColor: WidgetStateProperty.all(onPrimary), + overlayColor: WidgetStateProperty.all(onPrimary.withOpacity(.12)), + side: WidgetStateProperty.all(BorderSide(width: 1, color: onPrimary.withOpacity(.24))), ), - const SizedBox(height: 8), - OutlinedButtonTheme( - data: OutlinedButtonThemeData( - style: ButtonStyle( - foregroundColor: WidgetStateProperty.all(onPrimary), - overlayColor: WidgetStateProperty.all(onPrimary.withOpacity(.12)), - side: WidgetStateProperty.all(BorderSide(width: 1, color: onPrimary.withOpacity(.24))), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 6), + Align( + alignment: AlignmentDirectional.centerStart, + child: Wrap( + spacing: 16, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const AvesLogo(size: 48), + OutlinedText( + textSpans: [ + TextSpan( + text: l10n.appName, + style: TextStyle( + color: Colors.white, + fontSize: 38, + fontWeight: FontWeight.w300, + letterSpacing: canHaveLetterSpacing(context.locale) ? 1 : 0, + fontFeatures: const [FontFeature.enable('smcp')], + ), + ), + ], + ), + ], ), ), - child: Wrap( + const SizedBox(height: 8), + Wrap( spacing: 8, children: [ OutlinedButton.icon( @@ -191,9 +229,34 @@ class _AppDrawerState extends State { ), ], ), - ), - const SizedBox(height: 8), - ], + FutureBuilder>( + future: _profileSwitchFuture, + builder: (context, snapshot) { + final flags = snapshot.data; + if (flags == null) return const SizedBox(); + + final [ + bool canRequestInteractAcrossProfiles, + bool canSwitchProfile, + String profileSwitchingLabel, + ] = flags; + if ((!canRequestInteractAcrossProfiles && !canSwitchProfile) || profileSwitchingLabel.isEmpty) return const SizedBox(); + + return OutlinedButton( + onPressed: () async { + if (canSwitchProfile) { + await appProfileService.switchProfile(); + } else { + _profileSwitchPermissionRequested = await appProfileService.requestInteractAcrossProfiles(); + } + }, + child: Text(profileSwitchingLabel), + ); + }, + ), + const SizedBox(height: 8), + ], + ), ), ), );