Merge branch 'develop'
This commit is contained in:
commit
7a63164891
19 changed files with 476 additions and 149 deletions
4
.github/workflows/quality-check.yml
vendored
4
.github/workflows/quality-check.yml
vendored
|
@ -69,7 +69,7 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||
uses: github/codeql-action/init@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
|
@ -83,6 +83,6 @@ jobs:
|
|||
./flutterw build apk --profile -t lib/main_play.dart --flavor play
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||
uses: github/codeql-action/analyze@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
|
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
|
@ -71,6 +71,6 @@ jobs:
|
|||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- 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:
|
||||
sarif_file: results.sarif
|
||||
|
|
|
@ -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
|
||||
|
||||
## <a id="v1.11.13"></a>[v1.11.13] - 2024-09-17
|
||||
|
||||
|
|
|
@ -10,18 +10,6 @@ plugins {
|
|||
|
||||
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
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
|
@ -53,22 +41,12 @@ android {
|
|||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
lint {
|
||||
checkAllWarnings true
|
||||
warningsAsErrors true
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId packageName
|
||||
minSdk flutter.minSdkVersion
|
||||
targetSdk 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
versionCode flutter.versionCode
|
||||
versionName flutter.versionName
|
||||
manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"]
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
@ -189,7 +167,7 @@ dependencies {
|
|||
|
||||
implementation "androidx.appcompat:appcompat:1.7.0"
|
||||
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.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
|
|
|
@ -39,6 +39,10 @@
|
|||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING"
|
||||
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 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- from Android 12 (API 31), users can optionally grant access to the media management special permission -->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
89
lib/services/app_profile_service.dart
Normal file
89
lib/services/app_profile_service.dart
Normal 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 [];
|
||||
}
|
||||
}
|
|
@ -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<AvesVideoControl
|
|||
final AvesVideoMetadataFetcher videoMetadataFetcher = getIt<AvesVideoMetadataFetcher>();
|
||||
|
||||
final AppService appService = getIt<AppService>();
|
||||
final AppProfileService appProfileService = getIt<AppProfileService>();
|
||||
final DeviceService deviceService = getIt<DeviceService>();
|
||||
final EmbeddedDataService embeddedDataService = getIt<EmbeddedDataService>();
|
||||
final MediaEditService mediaEditService = getIt<MediaEditService>();
|
||||
|
@ -59,6 +61,7 @@ void initPlatformServices() {
|
|||
getIt.registerLazySingleton<AvesVideoMetadataFetcher>(FfmpegVideoMetadataFetcher.new);
|
||||
|
||||
getIt.registerLazySingleton<AppService>(PlatformAppService.new);
|
||||
getIt.registerLazySingleton<AppProfileService>(PlatformAppProfileService.new);
|
||||
getIt.registerLazySingleton<DeviceService>(PlatformDeviceService.new);
|
||||
getIt.registerLazySingleton<EmbeddedDataService>(PlatformEmbeddedDataService.new);
|
||||
getIt.registerLazySingleton<MediaEditService>(PlatformMediaEditService.new);
|
||||
|
|
|
@ -176,7 +176,6 @@ class PlatformStorageService implements StorageService {
|
|||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// returns number of deleted directories
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
133
lib/widgets/debug/capabilities.dart
Normal file
133
lib/widgets/debug/capabilities.dart
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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<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;
|
||||
final ValueNotifier<String> _queryNotifier = ValueNotifier('');
|
||||
|
||||
|
@ -37,7 +37,7 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> 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),
|
|
@ -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<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;
|
||||
final ValueNotifier<String> _queryNotifier = ValueNotifier('');
|
||||
|
||||
|
@ -34,7 +34,7 @@ class _DebugAndroidCodecSectionState extends State<DebugAndroidCodecSection> wit
|
|||
super.build(context);
|
||||
|
||||
return AvesExpansionTile(
|
||||
title: 'Android Codecs',
|
||||
title: 'OS Codecs',
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
|
@ -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<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;
|
||||
|
||||
@override
|
||||
|
@ -26,7 +26,7 @@ class _DebugAndroidDirSectionState extends State<DebugAndroidDirSection> 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),
|
|
@ -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<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 = {};
|
||||
|
||||
@override
|
||||
|
@ -31,7 +31,7 @@ class _DebugStorageSectionState extends State<DebugStorageSection> with Automati
|
|||
super.build(context);
|
||||
|
||||
return AvesExpansionTile(
|
||||
title: 'Storage Volumes',
|
||||
title: 'OS Storage',
|
||||
children: [
|
||||
...androidFileUtils.storageVolumes.expand((v) {
|
||||
final freeSpace = _freeSpaceByVolume[v.path];
|
|
@ -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<AppDrawer> {
|
||||
class _AppDrawerState extends State<AppDrawer> with WidgetsBindingObserver {
|
||||
// using the default controller conflicts
|
||||
// with bottom nav bar primary scroll monitoring
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
late Future<List<dynamic>> _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 = <Widget>[
|
||||
|
@ -133,44 +171,44 @@ class _AppDrawerState extends State<AppDrawer> {
|
|||
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<Color>(onPrimary),
|
||||
overlayColor: WidgetStateProperty.all<Color>(onPrimary.withOpacity(.12)),
|
||||
side: WidgetStateProperty.all<BorderSide>(BorderSide(width: 1, color: onPrimary.withOpacity(.24))),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButtonTheme(
|
||||
data: OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
foregroundColor: WidgetStateProperty.all<Color>(onPrimary),
|
||||
overlayColor: WidgetStateProperty.all<Color>(onPrimary.withOpacity(.12)),
|
||||
side: WidgetStateProperty.all<BorderSide>(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<AppDrawer> {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
FutureBuilder<List<dynamic>>(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue