device capabilities, API <19 prep

This commit is contained in:
Thibault Deckers 2021-11-28 14:38:17 +09:00
parent c5a4048322
commit 089304da2d
18 changed files with 168 additions and 122 deletions

View file

@ -44,7 +44,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
val messenger = backgroundFlutterEngine!!.dartExecutor.binaryMessenger val messenger = backgroundFlutterEngine!!.dartExecutor.binaryMessenger
// channels for analysis // channels for analysis
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler()) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))

View file

@ -59,7 +59,7 @@ class MainActivity : FlutterActivity() {
MethodChannel(messenger, AnalysisHandler.CHANNEL).setMethodCallHandler(analysisHandler) MethodChannel(messenger, AnalysisHandler.CHANNEL).setMethodCallHandler(analysisHandler)
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this)) MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this)) MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler()) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this)) MethodChannel(messenger, GlobalSearchHandler.CHANNEL).setMethodCallHandler(GlobalSearchHandler(this))
@ -163,11 +163,13 @@ class MainActivity : FlutterActivity() {
return return
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// save access permissions across reboots // save access permissions across reboots
val takeFlags = (data.flags val takeFlags = (data.flags
and (Intent.FLAG_GRANT_READ_URI_PERMISSION and (Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
contentResolver.takePersistableUriPermission(treeUri, takeFlags) contentResolver.takePersistableUriPermission(treeUri, takeFlags)
}
// resume pending action // resume pending action
onStorageAccessResult(requestCode, treeUri) onStorageAccessResult(requestCode, treeUri)

View file

@ -51,8 +51,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
"openMap" -> safe(call, result, ::openMap) "openMap" -> safe(call, result, ::openMap)
"setAs" -> safe(call, result, ::setAs) "setAs" -> safe(call, result, ::setAs)
"share" -> safe(call, result, ::share) "share" -> safe(call, result, ::share)
"canPin" -> safe(call, result, ::canPin) "pinShortcut" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pinShortcut) }
"pin" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::pin) }
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -323,13 +322,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// shortcuts // shortcuts
private fun isPinSupported() = ShortcutManagerCompat.isRequestPinShortcutSupported(context) private fun pinShortcut(call: MethodCall, result: MethodChannel.Result) {
private fun canPin(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(isPinSupported())
}
private fun pin(call: MethodCall, result: MethodChannel.Result) {
val label = call.argument<String>("label") val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes") val iconBytes = call.argument<ByteArray>("iconBytes")
val filters = call.argument<List<String>>("filters") val filters = call.argument<List<String>>("filters")
@ -339,7 +332,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
return return
} }
if (!isPinSupported()) { if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
result.error("pin-unsupported", "failed because the launcher does not support pinning shortcuts", null) result.error("pin-unsupported", "failed because the launcher does not support pinning shortcuts", null)
return return
} }

View file

@ -1,21 +1,36 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.Context
import android.os.Build import android.os.Build
import androidx.core.content.pm.ShortcutManagerCompat
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import java.util.* import java.util.*
class DeviceHandler : MethodCallHandler { class DeviceHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getCapabilities" -> safe(call, result, ::getCapabilities)
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone) "getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass) "getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
private fun getCapabilities(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(hashMapOf(
"canGrantDirectoryAccess" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP),
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
"canPrint" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT),
// as of google_maps_flutter v2.1.1, minSDK is 20 because of default PlatformView usage,
// but using hybrid composition would make it usable on API 19 too,
// cf https://github.com/flutter/flutter/issues/23728
"canRenderGoogleMaps" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH),
))
}
private fun getDefaultTimeZone(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun getDefaultTimeZone(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(TimeZone.getDefault().id) result.success(TimeZone.getDefault().id)
} }

View file

@ -73,6 +73,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.text.ParseException import java.text.ParseException
import java.util.* import java.util.*
@ -189,7 +190,15 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val kv = pair as KeyValuePair val kv = pair as KeyValuePair
val key = kv.key val key = kv.key
// `PNG-iTXt` uses UTF-8, contrary to `PNG-tEXt` and `PNG-zTXt` using Latin-1 / ISO-8859-1 // `PNG-iTXt` uses UTF-8, contrary to `PNG-tEXt` and `PNG-zTXt` using Latin-1 / ISO-8859-1
val charset = if (baseDirName == PNG_ITXT_DIR_NAME) StandardCharsets.UTF_8 else kv.value.charset val charset = if (baseDirName == PNG_ITXT_DIR_NAME) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
StandardCharsets.UTF_8
} else {
Charset.forName("UTF-8")
}
} else {
kv.value.charset
}
val valueString = String(kv.value.bytes, charset) val valueString = String(kv.value.bytes, charset)
val dirs = extractPngProfile(key, valueString) val dirs = extractPngProfile(key, valueString)
if (dirs?.any() == true) { if (dirs?.any() == true) {

View file

@ -91,6 +91,12 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
} }
private fun createFile() { private fun createFile() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
// TODO TLAD [<=API18] create file
error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
return
}
val name = args["name"] as String? val name = args["name"] as String?
val mimeType = args["mimeType"] as String? val mimeType = args["mimeType"] as String?
val bytes = args["bytes"] as ByteArray? val bytes = args["bytes"] as ByteArray?
@ -128,6 +134,12 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
private fun openFile() { private fun openFile() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
// TODO TLAD [<=API18] open file
error("openFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
return
}
val mimeType = args["mimeType"] as String? val mimeType = args["mimeType"] as String?
if (mimeType == null) { if (mimeType == null) {
error("openFile-args", "failed because of missing arguments", null) error("openFile-args", "failed because of missing arguments", null)

View file

@ -54,9 +54,11 @@ object MultiPage {
// do not use `MediaFormat.KEY_TRACK_ID` as it is actually not unique between tracks // do not use `MediaFormat.KEY_TRACK_ID` as it is actually not unique between tracks
// e.g. there could be both a video track and an image track with KEY_TRACK_ID == 1 // e.g. there could be both a video track and an image track with KEY_TRACK_ID == 1
format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 }
format.getSafeInt(MediaFormat.KEY_WIDTH) { track[KEY_WIDTH] = it } format.getSafeInt(MediaFormat.KEY_WIDTH) { track[KEY_WIDTH] = it }
format.getSafeInt(MediaFormat.KEY_HEIGHT) { track[KEY_HEIGHT] = it } format.getSafeInt(MediaFormat.KEY_HEIGHT) { track[KEY_HEIGHT] = it }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 }
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
format.getSafeInt(MediaFormat.KEY_ROTATION) { track[KEY_ROTATION_DEGREES] = it } format.getSafeInt(MediaFormat.KEY_ROTATION) { track[KEY_ROTATION_DEGREES] = it }
} }

View file

@ -195,21 +195,31 @@ object PermissionManager {
} ?: false } ?: false
} }
// returns paths matching URIs granted by the user // returns paths matching directory URIs granted by the user
fun getGrantedDirs(context: Context): Set<String> { fun getGrantedDirs(context: Context): Set<String> {
val grantedDirs = HashSet<String>() val grantedDirs = HashSet<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
for (uriPermission in context.contentResolver.persistedUriPermissions) { for (uriPermission in context.contentResolver.persistedUriPermissions) {
val dirPath = StorageUtils.convertTreeUriToDirPath(context, uriPermission.uri) val dirPath = StorageUtils.convertTreeUriToDirPath(context, uriPermission.uri)
dirPath?.let { grantedDirs.add(it) } dirPath?.let { grantedDirs.add(it) }
} }
}
return grantedDirs return grantedDirs
} }
// returns paths accessible to the app (granted by the user or by default) // returns paths accessible to the app (granted by the user or by default)
private fun getAccessibleDirs(context: Context): Set<String> { private fun getAccessibleDirs(context: Context): Set<String> {
val accessibleDirs = HashSet(getGrantedDirs(context)) val accessibleDirs = HashSet(getGrantedDirs(context))
// from Android R, we no longer have access permission by default on primary volume
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { // until API 18 / Android 4.3 / Jelly Bean MR2, removable storage is accessible by default like primary storage
// from API 19 / Android 4.4 / KitKat, removable storage requires access permission, at the file level
// from API 21 / Android 5.0 / Lollipop, removable storage requires access permission, but directory access grant is possible
// from API 30 / Android 11 / R, any storage requires access permission
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
accessibleDirs.addAll(StorageUtils.getVolumePaths(context))
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q
) {
accessibleDirs.add(StorageUtils.getPrimaryVolumePath(context)) accessibleDirs.add(StorageUtils.getPrimaryVolumePath(context))
} }
return accessibleDirs return accessibleDirs
@ -234,6 +244,7 @@ object PermissionManager {
} }
} }
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun releaseUriPermission(context: Context, it: Uri) { private fun releaseUriPermission(context: Context, it: Uri) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.releasePersistableUriPermission(it, flags) context.contentResolver.releasePersistableUriPermission(it, flags)

View file

@ -1,7 +1,7 @@
import 'package:aves/model/device.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:github/github.dart'; import 'package:github/github.dart';
@ -62,12 +62,8 @@ class LiveAvesAvailability implements AvesAvailability {
@override @override
Future<bool> get canLocatePlaces => Future.wait<bool>([isConnected, hasPlayServices]).then((results) => results.every((result) => result)); Future<bool> get canLocatePlaces => Future.wait<bool>([isConnected, hasPlayServices]).then((results) => results.every((result) => result));
// as of google_maps_flutter v2.1.1, minSDK is 20 because of default PlatformView usage,
// but using hybrid composition would make it usable on 19 too, cf https://github.com/flutter/flutter/issues/23728
Future<bool> get _isUseGoogleMapRenderingSupported => DeviceInfoPlugin().androidInfo.then((androidInfo) => (androidInfo.version.sdkInt ?? 0) >= 20);
@override @override
Future<bool> get canUseGoogleMaps => Future.wait<bool>([_isUseGoogleMapRenderingSupported, hasPlayServices]).then((results) => results.every((result) => result)); Future<bool> get canUseGoogleMaps async => device.canRenderGoogleMaps && await hasPlayServices;
@override @override
Future<bool> get isNewVersionAvailable async { Future<bool> get isNewVersionAvailable async {

32
lib/model/device.dart Normal file
View file

@ -0,0 +1,32 @@
import 'package:aves/services/common/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
final Device device = Device._private();
class Device {
late final String _userAgent;
late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderGoogleMaps;
String get userAgent => _userAgent;
bool get canGrantDirectoryAccess => _canGrantDirectoryAccess;
bool get canPinShortcut => _canPinShortcut;
bool get canPrint => _canPrint;
bool get canRenderGoogleMaps => _canRenderGoogleMaps;
Device._private();
Future<void> init() async {
final packageInfo = await PackageInfo.fromPlatform();
_userAgent = '${packageInfo.packageName}/${packageInfo.version}';
final capabilities = await deviceService.getCapabilities();
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
_canPrint = capabilities['canPrint'] ?? false;
_canRenderGoogleMaps = capabilities['canRenderGoogleMaps'] ?? false;
}
}

View file

@ -6,7 +6,6 @@ import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/math_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@ -29,8 +28,6 @@ abstract class AndroidAppService {
Future<bool> shareSingle(String uri, String mimeType); Future<bool> shareSingle(String uri, String mimeType);
Future<bool> canPinToHomeScreen();
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}); Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri});
} }
@ -174,25 +171,6 @@ class PlatformAndroidAppService implements AndroidAppService {
// app shortcuts // app shortcuts
// this ability will not change over the lifetime of the app
bool? _canPin;
@override
Future<bool> canPinToHomeScreen() async {
if (_canPin != null) return SynchronousFuture(_canPin!);
try {
final result = await platform.invokeMethod('canPin');
if (result != null) {
_canPin = result;
return result;
}
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
@override @override
Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}) async { Future<void> pinToHomeScreen(String label, AvesEntry? coverEntry, {Set<CollectionFilter>? filters, String? uri}) async {
Uint8List? iconBytes; Uint8List? iconBytes;
@ -209,7 +187,7 @@ class PlatformAndroidAppService implements AndroidAppService {
); );
} }
try { try {
await platform.invokeMethod('pin', <String, dynamic>{ await platform.invokeMethod('pinShortcut', <String, dynamic>{
'label': label, 'label': label,
'iconBytes': iconBytes, 'iconBytes': iconBytes,
'filters': filters?.map((filter) => filter.toJson()).toList(), 'filters': filters?.map((filter) => filter.toJson()).toList(),

View file

@ -2,6 +2,8 @@ import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
abstract class DeviceService { abstract class DeviceService {
Future<Map<String, dynamic>> getCapabilities();
Future<String?> getDefaultTimeZone(); Future<String?> getDefaultTimeZone();
Future<int> getPerformanceClass(); Future<int> getPerformanceClass();
@ -10,6 +12,17 @@ abstract class DeviceService {
class PlatformDeviceService implements DeviceService { class PlatformDeviceService implements DeviceService {
static const platform = MethodChannel('deckers.thibault/aves/device'); static const platform = MethodChannel('deckers.thibault/aves/device');
@override
Future<Map<String, dynamic>> getCapabilities() async {
try {
final result = await platform.invokeMethod('getCapabilities');
if (result != null) return (result as Map).cast<String, dynamic>();
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return {};
}
@override @override
Future<String?> getDefaultTimeZone() async { Future<String?> getDefaultTimeZone() async {
try { try {

View file

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/settings/accessibility_animations.dart'; import 'package:aves/model/settings/accessibility_animations.dart';
import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
@ -28,15 +29,12 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:overlay_support/overlay_support.dart'; import 'package:overlay_support/overlay_support.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class AvesApp extends StatefulWidget { class AvesApp extends StatefulWidget {
final AppFlavor flavor; final AppFlavor flavor;
static String userAgent = '';
const AvesApp({ const AvesApp({
Key? key, Key? key,
required this.flavor, required this.flavor,
@ -165,7 +163,7 @@ class _AvesAppState extends State<AvesApp> {
isRotationLocked: await windowService.isRotationLocked(), isRotationLocked: await windowService.isRotationLocked(),
areAnimationsRemoved: await AccessibilityService.areAnimationsRemoved(), areAnimationsRemoved: await AccessibilityService.areAnimationsRemoved(),
); );
unawaited(_initUserAgent()); await device.init();
FijkLog.setLevel(FijkLogLevel.Warn); FijkLog.setLevel(FijkLogLevel.Warn);
// keep screen on // keep screen on
@ -210,11 +208,6 @@ class _AvesAppState extends State<AvesApp> {
]; ];
} }
Future<void> _initUserAgent() async {
final info = await PackageInfo.fromPlatform();
AvesApp.userAgent = '${info.packageName}/${info.version}';
}
void _onNewIntent(Map? intentData) { void _onNewIntent(Map? intentData) {
debugPrint('$runtimeType onNewIntent with intentData=$intentData'); debugPrint('$runtimeType onNewIntent with intentData=$intentData');

View file

@ -10,7 +10,6 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.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/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
@ -46,7 +45,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate(); final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
late AnimationController _browseToSelectAnimation; late AnimationController _browseToSelectAnimation;
late Future<bool> _canAddShortcutsLoader;
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false); final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
final FocusNode _queryBarFocusNode = FocusNode(); final FocusNode _queryBarFocusNode = FocusNode();
late final Listenable _queryFocusRequestNotifier; late final Listenable _queryFocusRequestNotifier;
@ -69,7 +67,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
vsync: this, vsync: this,
); );
_isSelectingNotifier.addListener(_onActivityChange); _isSelectingNotifier.addListener(_onActivityChange);
_canAddShortcutsLoader = androidAppService.canPinToHomeScreen();
_registerWidget(widget); _registerWidget(widget);
WidgetsBinding.instance!.addPostFrameCallback((_) => _onFilterChanged()); WidgetsBinding.instance!.addPostFrameCallback((_) => _onFilterChanged());
} }
@ -104,10 +101,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appMode = context.watch<ValueNotifier<AppMode>>().value; final appMode = context.watch<ValueNotifier<AppMode>>().value;
return FutureBuilder<bool>(
future: _canAddShortcutsLoader,
builder: (context, snapshot) {
final canAddShortcuts = snapshot.data ?? false;
return Selector<Selection<AvesEntry>, Tuple2<bool, int>>( return Selector<Selection<AvesEntry>, Tuple2<bool, int>>(
selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length), selector: (context, selection) => Tuple2(selection.isSelecting, selection.selectedItems.length),
builder: (context, s, child) { builder: (context, s, child) {
@ -127,7 +120,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
actions: _buildActions( actions: _buildActions(
isSelecting: isSelecting, isSelecting: isSelecting,
selectedItemCount: selectedItemCount, selectedItemCount: selectedItemCount,
supportShortcuts: canAddShortcuts,
), ),
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: Size.fromHeight(appBarBottomHeight), preferredSize: Size.fromHeight(appBarBottomHeight),
@ -156,8 +148,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
); );
}, },
); );
},
);
} }
double get appBarBottomHeight { double get appBarBottomHeight {
@ -214,14 +204,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
List<Widget> _buildActions({ List<Widget> _buildActions({
required bool isSelecting, required bool isSelecting,
required int selectedItemCount, required int selectedItemCount,
required bool supportShortcuts,
}) { }) {
final appMode = context.watch<ValueNotifier<AppMode>>().value; final appMode = context.watch<ValueNotifier<AppMode>>().value;
bool isVisible(EntrySetAction action) => _actionDelegate.isVisible( bool isVisible(EntrySetAction action) => _actionDelegate.isVisible(
action, action,
appMode: appMode, appMode: appMode,
isSelecting: isSelecting, isSelecting: isSelecting,
supportShortcuts: supportShortcuts,
sortFactor: collection.sortFactor, sortFactor: collection.sortFactor,
itemCount: collection.entryCount, itemCount: collection.entryCount,
selectedItemCount: selectedItemCount, selectedItemCount: selectedItemCount,

View file

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_xmp_iptc.dart'; import 'package:aves/model/entry_xmp_iptc.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
@ -44,7 +45,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
EntrySetAction action, { EntrySetAction action, {
required AppMode appMode, required AppMode appMode,
required bool isSelecting, required bool isSelecting,
required bool supportShortcuts,
required EntrySortFactor sortFactor, required EntrySortFactor sortFactor,
required int itemCount, required int itemCount,
required int selectedItemCount, required int selectedItemCount,
@ -67,7 +67,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
case EntrySetAction.toggleTitleSearch: case EntrySetAction.toggleTitleSearch:
return !isSelecting; return !isSelecting;
case EntrySetAction.addShortcut: case EntrySetAction.addShortcut:
return appMode == AppMode.main && !isSelecting && supportShortcuts; return appMode == AppMode.main && !isSelecting && device.canPinShortcut;
// browsing or selecting // browsing or selecting
case EntrySetAction.map: case EntrySetAction.map:
case EntrySetAction.stats: case EntrySetAction.stats:

View file

@ -1,4 +1,4 @@
import 'package:aves/widgets/aves_app.dart'; import 'package:aves/model/device.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -53,7 +53,7 @@ class StamenWatercolorLayer extends StatelessWidget {
class _NetworkTileProvider extends NetworkTileProvider { class _NetworkTileProvider extends NetworkTileProvider {
final Map<String, String> headers = { final Map<String, String> headers = {
'User-Agent': AvesApp.userAgent, 'User-Agent': device.userAgent,
}; };
_NetworkTileProvider(); _NetworkTileProvider();

View file

@ -1,4 +1,5 @@
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/color_utils.dart';
@ -63,7 +64,7 @@ class PrivacySection extends StatelessWidget {
), ),
), ),
const HiddenItemsTile(), const HiddenItemsTile(),
const StorageAccessTile(), if (device.canGrantDirectoryAccess) const StorageAccessTile(),
], ],
); );
} }

View file

@ -1,4 +1,5 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
@ -79,7 +80,7 @@ class ViewerTopOverlay extends StatelessWidget {
return targetEntry.canRotateAndFlip; return targetEntry.canRotateAndFlip;
case EntryAction.export: case EntryAction.export:
case EntryAction.print: case EntryAction.print:
return !targetEntry.isVideo; return !targetEntry.isVideo && device.canPrint;
case EntryAction.openMap: case EntryAction.openMap:
return targetEntry.hasGps; return targetEntry.hasGps;
case EntryAction.viewSource: case EntryAction.viewSource: