device capabilities, API <19 prep
This commit is contained in:
parent
c5a4048322
commit
089304da2d
18 changed files with 168 additions and 122 deletions
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
32
lib/model/device.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue