Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-09-30 23:41:43 +02:00
commit 7a63164891
19 changed files with 476 additions and 149 deletions

View file

@ -69,7 +69,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 uses: github/codeql-action/init@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@ -83,6 +83,6 @@ jobs:
./flutterw build apk --profile -t lib/main_play.dart --flavor play ./flutterw build apk --profile -t lib/main_play.dart --flavor play
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 uses: github/codeql-action/analyze@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View file

@ -71,6 +71,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Map: OpenTopoMap layer - Map: OpenTopoMap layer
- Enterprise: support for work profile switching from the drawer
## <a id="v1.11.13"></a>[v1.11.13] - 2024-09-17 ## <a id="v1.11.13"></a>[v1.11.13] - 2024-09-17

View file

@ -10,18 +10,6 @@ plugins {
def packageName = "deckers.thibault.aves" def packageName = "deckers.thibault.aves"
// Flutter properties
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
def flutterVersionName = localProperties.getProperty('flutter.versionName')
// Keys // Keys
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
@ -53,22 +41,12 @@ android {
targetCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_21
} }
lint {
checkAllWarnings true
warningsAsErrors true
disable 'InvalidPackage'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig { defaultConfig {
applicationId packageName applicationId packageName
minSdk flutter.minSdkVersion minSdk flutter.minSdkVersion
targetSdk 35 targetSdk 35
versionCode flutterVersionCode.toInteger() versionCode flutter.versionCode
versionName flutterVersionName versionName flutter.versionName
manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"] manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"]
multiDexEnabled true multiDexEnabled true
} }
@ -189,7 +167,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.7.0" implementation "androidx.appcompat:appcompat:1.7.0"
implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.lifecycle:lifecycle-process:2.8.5' implementation 'androidx.lifecycle:lifecycle-process:2.8.6'
implementation 'androidx.media:media:1.7.0' implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06' implementation 'androidx.security:security-crypto:1.1.0-alpha06'

View file

@ -39,6 +39,10 @@
<uses-permission <uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING" android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING"
tools:ignore="SystemPermissionTypo" /> tools:ignore="SystemPermissionTypo" />
<!-- to switch between regular and work profiles -->
<uses-permission
android:name="android.permission.INTERACT_ACROSS_PROFILES"
tools:ignore="ProtectedPermissions" />
<!-- TODO TLAD still needed to fetch map tiles / reverse geocoding / else ? check in release mode --> <!-- TODO TLAD still needed to fetch map tiles / reverse geocoding / else ? check in release mode -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- from Android 12 (API 31), users can optionally grant access to the media management special permission --> <!-- from Android 12 (API 31), users can optionally grant access to the media management special permission -->

View file

@ -1,6 +1,7 @@
package deckers.thibault.aves package deckers.thibault.aves
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.KeyguardManager
import android.app.SearchManager import android.app.SearchManager
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.content.ClipData 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.AccessibilityHandler
import deckers.thibault.aves.channel.calls.AnalysisHandler import deckers.thibault.aves.channel.calls.AnalysisHandler
import deckers.thibault.aves.channel.calls.AppAdapterHandler 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.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.DebugHandler import deckers.thibault.aves.channel.calls.DebugHandler
import deckers.thibault.aves.channel.calls.DeviceHandler import deckers.thibault.aves.channel.calls.DeviceHandler
@ -142,6 +144,7 @@ open class MainActivity : FlutterFragmentActivity() {
MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this)) MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this)) MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
// - need Activity // - need Activity
MethodChannel(messenger, AppProfileHandler.CHANNEL).setMethodCallHandler(AppProfileHandler(this))
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this)) MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
// result streaming: dart -> platform ->->-> dart // result streaming: dart -> platform ->->-> dart
@ -318,7 +321,7 @@ open class MainActivity : FlutterFragmentActivity() {
INTENT_DATA_KEY_URI to uri.toString(), 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 val isLocked = keyguardManager.isKeyguardLocked
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(isLocked) setShowWhenLocked(isLocked)

View file

@ -1,6 +1,10 @@
package deckers.thibault.aves.channel.calls 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.pm.ApplicationInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
@ -42,7 +46,8 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.* import java.util.Locale
import java.util.UUID
import kotlin.math.roundToInt import kotlin.math.roundToInt
class AppAdapterHandler(private val context: Context) : MethodCallHandler { class AppAdapterHandler(private val context: Context) : MethodCallHandler {

View file

@ -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"
}
}

View file

@ -0,0 +1,89 @@
import 'dart:async';
import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
abstract class AppProfileService {
Future<bool> canInteractAcrossProfiles();
Future<bool> canRequestInteractAcrossProfiles();
Future<bool> requestInteractAcrossProfiles();
Future<String> getProfileSwitchingLabel();
Future<void> switchProfile();
Future<List<String>> getTargetUserProfiles();
}
class PlatformAppProfileService implements AppProfileService {
static const _platform = MethodChannel('deckers.thibault/aves/app_profile');
@override
Future<bool> 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<bool> 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<bool> 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<void> switchProfile() async {
try {
await _platform.invokeMethod('switchProfile');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return;
}
@override
Future<String> 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<List<String>> getTargetUserProfiles() async {
try {
final result = await _platform.invokeMethod('getTargetUserProfiles');
if (result != null) {
return (result as List).cast<String>();
}
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return [];
}
}

View file

@ -2,6 +2,7 @@ import 'package:aves/model/availability.dart';
import 'package:aves/model/db/db.dart'; import 'package:aves/model/db/db.dart';
import 'package:aves/model/db/db_sqflite.dart'; import 'package:aves/model/db/db_sqflite.dart';
import 'package:aves/model/settings/store_shared_pref.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/app_service.dart';
import 'package:aves/services/device_service.dart'; import 'package:aves/services/device_service.dart';
import 'package:aves/services/media/embedded_data_service.dart'; import 'package:aves/services/media/embedded_data_service.dart';
@ -37,6 +38,7 @@ final AvesVideoControllerFactory videoControllerFactory = getIt<AvesVideoControl
final AvesVideoMetadataFetcher videoMetadataFetcher = getIt<AvesVideoMetadataFetcher>(); final AvesVideoMetadataFetcher videoMetadataFetcher = getIt<AvesVideoMetadataFetcher>();
final AppService appService = getIt<AppService>(); final AppService appService = getIt<AppService>();
final AppProfileService appProfileService = getIt<AppProfileService>();
final DeviceService deviceService = getIt<DeviceService>(); final DeviceService deviceService = getIt<DeviceService>();
final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>(); final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
final MediaEditService mediaEditService = getIt<MediaEditService>(); final MediaEditService mediaEditService = getIt<MediaEditService>();
@ -59,6 +61,7 @@ void initPlatformServices() {
getIt.registerLazySingleton<AvesVideoMetadataFetcher>(FfmpegVideoMetadataFetcher.new); getIt.registerLazySingleton<AvesVideoMetadataFetcher>(FfmpegVideoMetadataFetcher.new);
getIt.registerLazySingleton<AppService>(PlatformAppService.new); getIt.registerLazySingleton<AppService>(PlatformAppService.new);
getIt.registerLazySingleton<AppProfileService>(PlatformAppProfileService.new);
getIt.registerLazySingleton<DeviceService>(PlatformDeviceService.new); getIt.registerLazySingleton<DeviceService>(PlatformDeviceService.new);
getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new); getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new);
getIt.registerLazySingleton<MediaEditService>(PlatformMediaEditService.new); getIt.registerLazySingleton<MediaEditService>(PlatformMediaEditService.new);

View file

@ -176,7 +176,6 @@ class PlatformStorageService implements StorageService {
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
return;
} }
// returns number of deleted directories // returns number of deleted directories

View file

@ -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/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart';
import 'package:aves/widgets/common/behaviour/pop/tv_navigation.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/app_debug_action.dart';
import 'package:aves/widgets/debug/cache.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/colors.dart';
import 'package:aves/widgets/debug/database.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/general.dart';
import 'package:aves/widgets/debug/media_store_scan_dialog.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/report.dart';
import 'package:aves/widgets/debug/settings.dart'; import 'package:aves/widgets/debug/settings.dart';
import 'package:aves/widgets/debug/storage.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -73,16 +73,16 @@ class AppDebugPage extends StatelessWidget {
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
children: const [ children: const [
DebugGeneralSection(), DebugGeneralSection(),
DebugAndroidAppSection(),
DebugAndroidCodecSection(),
DebugAndroidDirSection(),
DebugCacheSection(), DebugCacheSection(),
DebugCapabilitiesSection(),
DebugColorSection(), DebugColorSection(),
DebugAppDatabaseSection(), DebugAppDatabaseSection(),
DebugDeviceSection(),
DebugErrorReportingSection(), DebugErrorReportingSection(),
DebugSettingsSection(), DebugSettingsSection(),
DebugStorageSection(), DebugOSAppSection(),
DebugOSCodecSection(),
DebugOSPathSection(),
DebugOSStorageSection(),
], ],
), ),
), ),

View file

@ -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<DebugCapabilitiesSection> createState() => _DebugCapabilitiesSectionState();
}
class _DebugCapabilitiesSectionState extends State<DebugCapabilitiesSection> with AutomaticKeepAliveClientMixin {
late final Future<List<dynamic>> _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<List<dynamic>>(
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<List<dynamic>>(
future: _appFuture,
builder: (context, snapshot) {
final data = snapshot.data;
if (data == null) {
return const SizedBox();
}
final [
bool canInteractAcrossProfiles,
bool canRequestInteractAcrossProfiles,
List<String> 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;
}

View file

@ -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<DebugDeviceSection> createState() => _DebugDeviceSectionState();
}
class _DebugDeviceSectionState extends State<DebugDeviceSection> 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;
}

View file

@ -7,14 +7,14 @@ import 'package:aves/widgets/viewer/info/common.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class DebugAndroidAppSection extends StatefulWidget { class DebugOSAppSection extends StatefulWidget {
const DebugAndroidAppSection({super.key}); const DebugOSAppSection({super.key});
@override @override
State<DebugAndroidAppSection> createState() => _DebugAndroidAppSectionState(); State<DebugOSAppSection> createState() => _DebugOSAppSectionState();
} }
class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with AutomaticKeepAliveClientMixin { class _DebugOSAppSectionState extends State<DebugOSAppSection> with AutomaticKeepAliveClientMixin {
late Future<Set<Package>> _loader; late Future<Set<Package>> _loader;
final ValueNotifier<String> _queryNotifier = ValueNotifier(''); final ValueNotifier<String> _queryNotifier = ValueNotifier('');
@ -37,7 +37,7 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with Au
super.build(context); super.build(context);
return AvesExpansionTile( return AvesExpansionTile(
title: 'Android Apps', title: 'OS Apps',
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),

View file

@ -6,14 +6,14 @@ import 'package:aves/widgets/viewer/info/common.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class DebugAndroidCodecSection extends StatefulWidget { class DebugOSCodecSection extends StatefulWidget {
const DebugAndroidCodecSection({super.key}); const DebugOSCodecSection({super.key});
@override @override
State<DebugAndroidCodecSection> createState() => _DebugAndroidCodecSectionState(); State<DebugOSCodecSection> createState() => _DebugOSCodecSectionState();
} }
class _DebugAndroidCodecSectionState extends State<DebugAndroidCodecSection> with AutomaticKeepAliveClientMixin { class _DebugOSCodecSectionState extends State<DebugOSCodecSection> with AutomaticKeepAliveClientMixin {
late Future<List<Map>> _loader; late Future<List<Map>> _loader;
final ValueNotifier<String> _queryNotifier = ValueNotifier(''); final ValueNotifier<String> _queryNotifier = ValueNotifier('');
@ -34,7 +34,7 @@ class _DebugAndroidCodecSectionState extends State<DebugAndroidCodecSection> wit
super.build(context); super.build(context);
return AvesExpansionTile( return AvesExpansionTile(
title: 'Android Codecs', title: 'OS Codecs',
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),

View file

@ -5,14 +5,14 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class DebugAndroidDirSection extends StatefulWidget { class DebugOSPathSection extends StatefulWidget {
const DebugAndroidDirSection({super.key}); const DebugOSPathSection({super.key});
@override @override
State<DebugAndroidDirSection> createState() => _DebugAndroidDirSectionState(); State<DebugOSPathSection> createState() => _DebugOSPathSectionState();
} }
class _DebugAndroidDirSectionState extends State<DebugAndroidDirSection> with AutomaticKeepAliveClientMixin { class _DebugOSPathSectionState extends State<DebugOSPathSection> with AutomaticKeepAliveClientMixin {
late Future<Map> _loader; late Future<Map> _loader;
@override @override
@ -26,7 +26,7 @@ class _DebugAndroidDirSectionState extends State<DebugAndroidDirSection> with Au
super.build(context); super.build(context);
return AvesExpansionTile( return AvesExpansionTile(
title: 'Android Dirs', title: 'OS Paths',
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),

View file

@ -7,14 +7,14 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class DebugStorageSection extends StatefulWidget { class DebugOSStorageSection extends StatefulWidget {
const DebugStorageSection({super.key}); const DebugOSStorageSection({super.key});
@override @override
State<DebugStorageSection> createState() => _DebugStorageSectionState(); State<DebugOSStorageSection> createState() => _DebugOSStorageSectionState();
} }
class _DebugStorageSectionState extends State<DebugStorageSection> with AutomaticKeepAliveClientMixin { class _DebugOSStorageSectionState extends State<DebugOSStorageSection> with AutomaticKeepAliveClientMixin {
final Map<String, int?> _freeSpaceByVolume = {}; final Map<String, int?> _freeSpaceByVolume = {};
@override @override
@ -31,7 +31,7 @@ class _DebugStorageSectionState extends State<DebugStorageSection> with Automati
super.build(context); super.build(context);
return AvesExpansionTile( return AvesExpansionTile(
title: 'Storage Volumes', title: 'OS Storage',
children: [ children: [
...androidFileUtils.storageVolumes.expand((v) { ...androidFileUtils.storageVolumes.expand((v) {
final freeSpace = _freeSpaceByVolume[v.path]; final freeSpace = _freeSpaceByVolume[v.path];

View file

@ -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/location/place.dart';
import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/tag.dart';
import 'package:aves/ref/locales.dart'; import 'package:aves/ref/locales.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
@ -56,13 +57,50 @@ class AppDrawer extends StatefulWidget {
} }
} }
class _AppDrawerState extends State<AppDrawer> { class _AppDrawerState extends State<AppDrawer> with WidgetsBindingObserver {
// using the default controller conflicts // using the default controller conflicts
// with bottom nav bar primary scroll monitoring // with bottom nav bar primary scroll monitoring
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
late Future<List<dynamic>> _profileSwitchFuture;
bool _profileSwitchPermissionRequested = false;
CollectionLens? get currentCollection => widget.currentCollection; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final drawerItems = <Widget>[ final drawerItems = <Widget>[
@ -133,44 +171,44 @@ class _AppDrawerState extends State<AppDrawer> {
color: colorScheme.primary, color: colorScheme.primary,
child: SafeArea( child: SafeArea(
bottom: false, bottom: false,
child: Column( child: OutlinedButtonTheme(
crossAxisAlignment: CrossAxisAlignment.start, data: OutlinedButtonThemeData(
children: [ style: ButtonStyle(
const SizedBox(height: 6), foregroundColor: WidgetStateProperty.all<Color>(onPrimary),
Align( overlayColor: WidgetStateProperty.all<Color>(onPrimary.withOpacity(.12)),
alignment: AlignmentDirectional.centerStart, side: WidgetStateProperty.all<BorderSide>(BorderSide(width: 1, color: onPrimary.withOpacity(.24))),
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')],
),
),
],
),
],
),
), ),
const SizedBox(height: 8), ),
OutlinedButtonTheme( child: Column(
data: OutlinedButtonThemeData( crossAxisAlignment: CrossAxisAlignment.start,
style: ButtonStyle( children: [
foregroundColor: WidgetStateProperty.all<Color>(onPrimary), const SizedBox(height: 6),
overlayColor: WidgetStateProperty.all<Color>(onPrimary.withOpacity(.12)), Align(
side: WidgetStateProperty.all<BorderSide>(BorderSide(width: 1, color: onPrimary.withOpacity(.24))), 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, spacing: 8,
children: [ children: [
OutlinedButton.icon( OutlinedButton.icon(
@ -191,9 +229,34 @@ class _AppDrawerState extends State<AppDrawer> {
), ),
], ],
), ),
), FutureBuilder<List<dynamic>>(
const SizedBox(height: 8), 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),
],
),
), ),
), ),
); );