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 ccc38f8b1..cc0fbf9bc 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 @@ -12,6 +12,7 @@ import androidx.core.content.pm.ShortcutManagerCompat import com.google.android.material.color.DynamicColors import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.model.FieldMap +import deckers.thibault.aves.utils.MimeTypes import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -55,6 +56,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler { "canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP), "hasGeocoder" to Geocoder.isPresent(), "isDynamicColorAvailable" to DynamicColors.isDynamicColorAvailable(), + "regionDecodableMimeTypes" to MimeTypes.supportedByBitmapRegionDecoder, "showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O), "supportEdgeToEdgeUIMode" to (sdkInt >= Build.VERSION_CODES.Q), ) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index 9d219a7a2..59bce9ebb 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -163,4 +163,13 @@ object MimeTypes { } val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE) + + val supportedByBitmapRegionDecoder: List = listOf( + // Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported" + // but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below, + // and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested. + HEIC, HEIF, JPEG, PNG, WEBP, ARW, CR2, NEF, NRW, ORF, PEF, RAF, RW2, SRW, + // custom support + TIFF, + ) } diff --git a/lib/model/app/support.dart b/lib/model/app/support.dart index fbf2a29b4..170e82b85 100644 --- a/lib/model/app/support.dart +++ b/lib/model/app/support.dart @@ -20,28 +20,6 @@ class AppSupport { static bool canDecode(String mimeType) => !undecodableImages.contains(mimeType); - // Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported" - // but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below, - // and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested. - static bool _supportedByBitmapRegionDecoder(String mimeType) => [ - MimeTypes.heic, - MimeTypes.heif, - MimeTypes.jpeg, - MimeTypes.png, - MimeTypes.webp, - MimeTypes.arw, - MimeTypes.cr2, - MimeTypes.nef, - MimeTypes.nrw, - MimeTypes.orf, - MimeTypes.pef, - MimeTypes.raf, - MimeTypes.rw2, - MimeTypes.srw, - ].contains(mimeType); - - static bool canDecodeRegion(String mimeType) => _supportedByBitmapRegionDecoder(mimeType) || mimeType == MimeTypes.tiff; - // `exifinterface` v1.3.3 declared support for DNG, but it strips non-standard Exif tags when saving attributes, // and DNG requires DNG-specific tags saved along standard Exif. So it was actually breaking DNG files. static bool canEditExif(String mimeType) { diff --git a/lib/model/device.dart b/lib/model/device.dart index e2c687e21..8137133c4 100644 --- a/lib/model/device.dart +++ b/lib/model/device.dart @@ -12,6 +12,7 @@ class Device { late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut; late final bool _canRenderFlagEmojis, _canRenderSubdivisionFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto; late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture; + late final List _regionDecodableMimeTypes; String get packageName => _packageName; @@ -49,6 +50,8 @@ class Device { bool get supportPictureInPicture => _supportPictureInPicture; + bool canDecodeRegion(String mimeType) => _regionDecodableMimeTypes.contains(mimeType); + Device._private(); Future init() async { @@ -82,6 +85,7 @@ class Device { _canUseCrypto = capabilities['canUseCrypto'] ?? false; _hasGeocoder = capabilities['hasGeocoder'] ?? false; _isDynamicColorAvailable = capabilities['isDynamicColorAvailable'] ?? false; + _regionDecodableMimeTypes = (capabilities['regionDecodableMimeTypes'] ?? []).cast(); _showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false; _supportEdgeToEdgeUIMode = capabilities['supportEdgeToEdgeUIMode'] ?? false; } diff --git a/lib/model/entry/extensions/props.dart b/lib/model/entry/extensions/props.dart index 16e71676a..bdcc48fce 100644 --- a/lib/model/entry/extensions/props.dart +++ b/lib/model/entry/extensions/props.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:ui'; import 'package:aves/model/app/support.dart'; +import 'package:aves/model/device.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/trash.dart'; @@ -127,7 +128,7 @@ extension ExtraAvesEntryProps on AvesEntry { bool get canDecode => AppSupport.canDecode(mimeType); - bool get canDecodeRegion => AppSupport.canDecodeRegion(mimeType) && !isAnimated; + bool get canDecodeRegion => device.canDecodeRegion(mimeType) && !isAnimated; bool get canEditExif => AppSupport.canEditExif(mimeType); diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index bd0ab2e00..b625314ce 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -19,6 +19,7 @@ import 'package:aves/widgets/debug/app_debug_action.dart'; import 'package:aves/widgets/debug/cache.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/report.dart'; @@ -75,6 +76,7 @@ class AppDebugPage extends StatelessWidget { DebugCacheSection(), DebugColorSection(), DebugAppDatabaseSection(), + DebugDeviceSection(), DebugErrorReportingSection(), DebugSettingsSection(), DebugStorageSection(), diff --git a/lib/widgets/debug/device.dart b/lib/widgets/debug/device.dart new file mode 100644 index 000000000..c9bf6d9d4 --- /dev/null +++ b/lib/widgets/debug/device.dart @@ -0,0 +1,51 @@ +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}', + 'canGrantDirectoryAccess': '${device.canGrantDirectoryAccess}', + 'canPinShortcut': '${device.canPinShortcut}', + 'canRenderFlagEmojis': '${device.canRenderFlagEmojis}', + 'canRenderSubdivisionFlagEmojis': '${device.canRenderSubdivisionFlagEmojis}', + 'canRequestManageMedia': '${device.canRequestManageMedia}', + 'canSetLockScreenWallpaper': '${device.canSetLockScreenWallpaper}', + 'canUseCrypto': '${device.canUseCrypto}', + 'canUseVaults': '${device.canUseVaults}', + 'hasGeocoder': '${device.hasGeocoder}', + 'isDynamicColorAvailable': '${device.isDynamicColorAvailable}', + 'isTelevision': '${device.isTelevision}', + 'showPinShortcutFeedback': '${device.showPinShortcutFeedback}', + 'supportEdgeToEdgeUIMode': '${device.supportEdgeToEdgeUIMode}', + 'supportPictureInPicture': '${device.supportPictureInPicture}', + }, + ), + ), + ], + ); + } + + @override + bool get wantKeepAlive => true; +}