diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt index e9c9fbc26..be8ee35ee 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DeviceHandler.kt @@ -1,6 +1,7 @@ package deckers.thibault.aves.channel.calls import android.content.Context +import android.content.Intent import android.content.res.Resources import android.os.Build import androidx.core.content.pm.ShortcutManagerCompat @@ -18,6 +19,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { "getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone) "getLocales" -> safe(call, result, ::getLocales) "getPerformanceClass" -> safe(call, result, ::getPerformanceClass) + "isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled) else -> result.notImplemented() } } @@ -34,7 +36,6 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { // but using hybrid composition would make it usable on API 19 too, // cf https://github.com/flutter/flutter/issues/23728 "canRenderGoogleMaps" to (sdkInt >= Build.VERSION_CODES.KITKAT_WATCH), - "hasFilePicker" to (sdkInt >= Build.VERSION_CODES.KITKAT), "showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O), "supportEdgeToEdgeUIMode" to (sdkInt >= Build.VERSION_CODES.Q), ) @@ -82,6 +83,15 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { result.success(Build.VERSION.SDK_INT) } + private fun isSystemFilePickerEnabled(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { + val enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).resolveActivity(context.packageManager) != null + } else { + false + } + result.success(enabled) + } + companion object { const val CHANNEL = "deckers.thibault/aves/device" } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt index 8970a02d9..6d134fae4 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt @@ -170,7 +170,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, ::onGranted, ::onDenied) activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST) } else { - MainActivity.notifyError("failed to resolve activity for intent=$intent") + MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}") onDenied() } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt index e77a1648e..02eeaf485 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt @@ -55,7 +55,7 @@ object PermissionManager { MainActivity.pendingStorageAccessResultHandlers[MainActivity.DOCUMENT_TREE_ACCESS_REQUEST] = PendingStorageAccessResultHandler(path, onGranted, onDenied) activity.startActivityForResult(intent, MainActivity.DOCUMENT_TREE_ACCESS_REQUEST) } else { - MainActivity.notifyError("failed to resolve activity for intent=$intent") + MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}") onDenied() } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 21d0beac4..f5354e8e1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -204,6 +204,8 @@ } } }, + "missingSystemFilePickerDialogTitle": "Missing System File Picker", + "missingSystemFilePickerDialogMessage": "The system file picker is missing or disabled. Please enable it and try again.", "unsupportedTypeDialogTitle": "Unsupported Types", "unsupportedTypeDialogMessage": "{count, plural, =1{This operation is not supported for items of the following type: {types}.} other{This operation is not supported for items of the following types: {types}.}}", diff --git a/lib/model/device.dart b/lib/model/device.dart index 7a8ab076b..e26583f61 100644 --- a/lib/model/device.dart +++ b/lib/model/device.dart @@ -6,7 +6,7 @@ final Device device = Device._private(); class Device { late final String _userAgent; late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis, _canRenderGoogleMaps; - late final bool _hasFilePicker, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode; + late final bool _showPinShortcutFeedback, _supportEdgeToEdgeUIMode; String get userAgent => _userAgent; @@ -20,8 +20,6 @@ class Device { bool get canRenderGoogleMaps => _canRenderGoogleMaps; - bool get hasFilePicker => _hasFilePicker; - bool get showPinShortcutFeedback => _showPinShortcutFeedback; bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode; @@ -38,7 +36,6 @@ class Device { _canPrint = capabilities['canPrint'] ?? false; _canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false; _canRenderGoogleMaps = capabilities['canRenderGoogleMaps'] ?? false; - _hasFilePicker = capabilities['hasFilePicker'] ?? false; _showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false; _supportEdgeToEdgeUIMode = capabilities['supportEdgeToEdgeUIMode'] ?? false; } diff --git a/lib/services/device_service.dart b/lib/services/device_service.dart index 88cc22522..3360204f2 100644 --- a/lib/services/device_service.dart +++ b/lib/services/device_service.dart @@ -11,6 +11,8 @@ abstract class DeviceService { Future> getLocales(); Future getPerformanceClass(); + + Future isSystemFilePickerEnabled(); } class PlatformDeviceService implements DeviceService { @@ -60,7 +62,6 @@ class PlatformDeviceService implements DeviceService { @override Future getPerformanceClass() async { try { - await platform.invokeMethod('getPerformanceClass'); final result = await platform.invokeMethod('getPerformanceClass'); if (result != null) return result as int; } on PlatformException catch (e, stack) { @@ -68,4 +69,15 @@ class PlatformDeviceService implements DeviceService { } return 0; } + + @override + Future isSystemFilePickerEnabled() async { + try { + final result = await platform.invokeMethod('isSystemFilePickerEnabled'); + if (result != null) return result as bool; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return false; + } } diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index ab41e0717..329c325f0 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -47,11 +47,12 @@ mixin PermissionAwareMixin { final confirmed = await showDialog( context: context, builder: (context) { - final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir); + final l10n = context.l10n; + final directory = dir.relativeDir.isEmpty ? l10n.rootDirectoryDescription : l10n.otherDirectoryDescription(dir.relativeDir); final volume = dir.getVolumeDescription(context); return AvesDialog( - title: context.l10n.storageAccessDialogTitle, - content: Text(context.l10n.storageAccessDialogMessage(directory, volume)), + title: l10n.storageAccessDialogTitle, + content: Text(l10n.storageAccessDialogMessage(directory, volume)), actions: [ TextButton( onPressed: () => Navigator.pop(context), @@ -68,6 +69,26 @@ mixin PermissionAwareMixin { // abort if the user cancels in Flutter if (confirmed == null || !confirmed) return false; + if (!await deviceService.isSystemFilePickerEnabled()) { + await showDialog( + context: context, + builder: (context) { + final l10n = context.l10n; + return AvesDialog( + title: l10n.missingSystemFilePickerDialogTitle, + content: Text(l10n.missingSystemFilePickerDialogMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(MaterialLocalizations.of(context).okButtonLabel), + ), + ], + ); + }, + ); + return false; + } + final granted = await storageService.requestDirectoryAccess(dir.volumePath); if (!granted) { // abort if the user denies access from the native dialog diff --git a/lib/widgets/debug/android_apps.dart b/lib/widgets/debug/android_apps.dart index 104721b4f..25a89ef64 100644 --- a/lib/widgets/debug/android_apps.dart +++ b/lib/widgets/debug/android_apps.dart @@ -51,7 +51,7 @@ class _DebugAndroidAppSectionState extends State with Au return ValueListenableBuilder( valueListenable: _queryNotifier, builder: (context, query, child) { - if ({package.packageName, ...package.potentialDirs}.none((v) => v.contains(query))) { + if ({package.packageName, ...package.potentialDirs}.none((v) => v.toLowerCase().contains(query.toLowerCase()))) { return const SizedBox(); } return Text.rich( diff --git a/untranslated.json b/untranslated.json index f1c5f26d0..a0db63d02 100644 --- a/untranslated.json +++ b/untranslated.json @@ -2,6 +2,8 @@ "de": [ "filterRatingUnratedLabel", "filterRatingRejectedLabel", + "missingSystemFilePickerDialogTitle", + "missingSystemFilePickerDialogMessage", "editEntryDateDialogSourceFieldLabel", "editEntryDateDialogSourceCustomDate", "editEntryDateDialogSourceTitle", @@ -12,9 +14,21 @@ "settingsThumbnailShowRating" ], + "fr": [ + "missingSystemFilePickerDialogTitle", + "missingSystemFilePickerDialogMessage" + ], + + "ko": [ + "missingSystemFilePickerDialogTitle", + "missingSystemFilePickerDialogMessage" + ], + "ru": [ "filterRatingUnratedLabel", "filterRatingRejectedLabel", + "missingSystemFilePickerDialogTitle", + "missingSystemFilePickerDialogMessage", "editEntryDateDialogSourceFieldLabel", "editEntryDateDialogSourceCustomDate", "editEntryDateDialogSourceTitle",