API 16 support prep

This commit is contained in:
Thibault Deckers 2021-11-28 19:32:04 +09:00
parent 089304da2d
commit 35958d87fd
20 changed files with 143 additions and 91 deletions

View file

@ -56,8 +56,7 @@ android {
// minSdkVersion constraints:
// - Flutter & other plugins: 16
// - google_maps_flutter v2.1.1: 20
// - Aves native: 19
minSdkVersion 19
minSdkVersion 16
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@ -149,7 +148,7 @@ dependencies {
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a'
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android
implementation 'com.github.deckerst:pixymeta-android:a86b1b8e4c'
implementation 'com.github.deckerst:pixymeta-android:706bd73d6e'
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'androidx.annotation:annotation:1.3.0'

View file

@ -23,7 +23,6 @@ import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.runBlocking
import java.util.*
class AnalysisService : MethodChannel.MethodCallHandler, Service() {
private var backgroundFlutterEngine: FlutterEngine? = null
@ -141,11 +140,12 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
getString(R.string.analysis_notification_action_stop),
stopServiceIntent
).build()
val icon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) R.drawable.ic_notification else R.mipmap.ic_launcher_round
return NotificationCompat.Builder(this, CHANNEL_ANALYSIS)
.setContentTitle(title ?: getText(R.string.analysis_notification_default_title))
.setContentText(message)
.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
.setSmallIcon(R.drawable.ic_notification)
.setSmallIcon(icon)
.setContentIntent(openAppIntent)
.setPriority(NotificationCompat.PRIORITY_LOW)
.addAction(stopAction)

View file

@ -24,10 +24,12 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
private fun areAnimationsRemoved(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
var removed = false
try {
removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
try {
removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
}
}
result.success(removed)
}

View file

@ -62,7 +62,14 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
fun addPackageDetails(intent: Intent) {
// apps tend to use their name in English when creating directories
// so we get their names in English as well as the current locale
val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) }
val englishConfig = Configuration().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
setLocale(Locale.ENGLISH)
} else {
@Suppress("deprecation")
locale = Locale.ENGLISH
}
}
val pm = context.packageManager
for (resolveInfo in pm.queryIntentActivities(intent, 0)) {

View file

@ -20,15 +20,21 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
}
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),
))
val sdkInt = Build.VERSION.SDK_INT
result.success(
hashMapOf(
"canGrantDirectoryAccess" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
"canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT),
"canRenderEmojis" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
// 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 (sdkInt >= Build.VERSION_CODES.KITKAT_WATCH),
"hasFilePicker" to (sdkInt >= Build.VERSION_CODES.KITKAT),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
)
)
}
private fun getDefaultTimeZone(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {

View file

@ -584,7 +584,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
try {
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it }
}
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { metadataMap[KEY_DATE_MILLIS] = it }
}

View file

@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.streams
import android.content.Context
import android.database.ContentObserver
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.Settings
@ -32,12 +33,13 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
override fun onChange(selfChange: Boolean, uri: Uri?) {
if (update()) {
success(
hashMapOf(
Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
Settings.Global.TRANSITION_ANIMATION_SCALE to transitionAnimationScale,
)
val settings: FieldMap = hashMapOf(
Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
settings[Settings.Global.TRANSITION_ANIMATION_SCALE] = transitionAnimationScale
}
success(settings)
}
}
@ -49,12 +51,13 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
accelerometerRotation = newAccelerometerRotation
changed = true
}
val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE)
if (transitionAnimationScale != newTransitionAnimationScale) {
transitionAnimationScale = newTransitionAnimationScale
changed = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE)
if (transitionAnimationScale != newTransitionAnimationScale) {
transitionAnimationScale = newTransitionAnimationScale
changed = true
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
}

View file

@ -27,11 +27,13 @@ object MediaMetadataRetrieverHelper {
MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS to "Number of Tracks",
MediaMetadataRetriever.METADATA_KEY_TITLE to "Title",
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT to "Video Height",
MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION to "Video Rotation",
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH to "Video Width",
MediaMetadataRetriever.METADATA_KEY_WRITER to "Writer",
MediaMetadataRetriever.METADATA_KEY_YEAR to "Year",
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
put(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION, "Video Rotation")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
put(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE, "Capture Framerate")
}

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import androidx.exifinterface.media.ExifInterface
import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.avi.AviDirectory
@ -135,10 +136,12 @@ class SourceEntry {
try {
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) { width = it }
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) { height = it }
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { sourceRotationDegrees = it }
retriever.getSafeLong(MediaMetadataRetriever.METADATA_KEY_DURATION) { durationMillis = it }
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { sourceDateTakenMillis = it }
retriever.getSafeString(MediaMetadataRetriever.METADATA_KEY_TITLE) { title = it }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { sourceRotationDegrees = it }
}
} catch (e: Exception) {
// ignore
} finally {

View file

@ -142,39 +142,6 @@ object PermissionManager {
}
}
fun getRestrictedDirectories(context: Context): List<Map<String, String>> {
val dirs = ArrayList<Map<String, String>>()
val sdkInt = Build.VERSION.SDK_INT
if (sdkInt >= Build.VERSION_CODES.R) {
// cf https://developer.android.com/about/versions/11/privacy/storage#directory-access
val volumePaths = StorageUtils.getVolumePaths(context)
dirs.addAll(volumePaths.map {
hashMapOf(
"volumePath" to it,
"relativeDir" to "",
)
})
dirs.addAll(volumePaths.map {
hashMapOf(
"volumePath" to it,
"relativeDir" to Environment.DIRECTORY_DOWNLOADS,
)
})
} else if (sdkInt == Build.VERSION_CODES.KITKAT || sdkInt == Build.VERSION_CODES.KITKAT_WATCH) {
// no SD card volume access on KitKat
val primaryVolume = StorageUtils.getPrimaryVolumePath(context)
val nonPrimaryVolumes = StorageUtils.getVolumePaths(context).filter { it != primaryVolume }
dirs.addAll(nonPrimaryVolumes.map {
hashMapOf(
"volumePath" to it,
"relativeDir" to "",
)
})
}
return dirs
}
fun canInsertByMediaStore(directories: List<FieldMap>): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
directories.all {
@ -217,14 +184,46 @@ object PermissionManager {
// 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
) {
} else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
accessibleDirs.add(StorageUtils.getPrimaryVolumePath(context))
}
return accessibleDirs
}
fun getRestrictedDirectories(context: Context): List<Map<String, String>> {
val dirs = ArrayList<Map<String, String>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// cf https://developer.android.com/about/versions/11/privacy/storage#directory-access
val volumePaths = StorageUtils.getVolumePaths(context)
dirs.addAll(volumePaths.map {
hashMapOf(
"volumePath" to it,
"relativeDir" to "",
)
})
dirs.addAll(volumePaths.map {
hashMapOf(
"volumePath" to it,
"relativeDir" to Environment.DIRECTORY_DOWNLOADS,
)
})
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT_WATCH) {
// removable storage requires access permission, at the file level
// without directory access, we consider the whole volume restricted
val primaryVolume = StorageUtils.getPrimaryVolumePath(context)
val nonPrimaryVolumes = StorageUtils.getVolumePaths(context).filter { it != primaryVolume }
dirs.addAll(nonPrimaryVolumes.map {
hashMapOf(
"volumePath" to it,
"relativeDir" to "",
)
})
}
return dirs
}
// As of Android R, `MediaStore.getDocumentUri` fails if any of the persisted
// URI permissions we hold points to a folder that no longer exists,
// so we should remove these obsolete URIs before proceeding.

View file

@ -5,7 +5,8 @@ final Device device = Device._private();
class Device {
late final String _userAgent;
late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderGoogleMaps;
late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderEmojis, _canRenderGoogleMaps;
late final bool _hasFilePicker, _showPinShortcutFeedback;
String get userAgent => _userAgent;
@ -15,8 +16,15 @@ class Device {
bool get canPrint => _canPrint;
bool get canRenderEmojis => _canRenderEmojis;
bool get canRenderGoogleMaps => _canRenderGoogleMaps;
// TODO TLAD toggle settings > import/export, about > bug report > save
bool get hasFilePicker => _hasFilePicker;
bool get showPinShortcutFeedback => _showPinShortcutFeedback;
Device._private();
Future<void> init() async {
@ -27,6 +35,9 @@ class Device {
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
_canPrint = capabilities['canPrint'] ?? false;
_canRenderEmojis = capabilities['canRenderEmojis'] ?? false;
_canRenderGoogleMaps = capabilities['canRenderGoogleMaps'] ?? false;
_hasFilePicker = capabilities['hasFilePicker'] ?? false;
_showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false;
}
}

View file

@ -31,6 +31,8 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
];
static CollectionFilter? fromJson(String jsonString) {
if (jsonString.isEmpty) return null;
try {
final jsonMap = jsonDecode(jsonString);
if (jsonMap is Map<String, dynamic>) {

View file

@ -1,3 +1,4 @@
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
@ -58,15 +59,17 @@ class LocationFilter extends CollectionFilter {
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) {
final flag = countryCodeToFlag(_countryCode);
// as of Flutter v1.22.3, emoji shadows are rendered as colorful duplicates,
// not filled with the shadow color as expected, so we remove them
if (flag != null) {
return Text(
flag,
style: TextStyle(fontSize: size, shadows: const []),
textScaleFactor: 1.0,
);
if (_countryCode != null && device.canRenderEmojis) {
final flag = countryCodeToFlag(_countryCode);
// as of Flutter v1.22.3, emoji shadows are rendered as colorful duplicates,
// not filled with the shadow color as expected, so we remove them
if (flag != null) {
return Text(
flag,
style: TextStyle(fontSize: size, shadows: const []),
textScaleFactor: 1.0,
);
}
}
return Icon(_location.isEmpty ? AIcons.locationOff : AIcons.location, size: size);
}

View file

@ -8,8 +8,8 @@ import 'package:aves/model/source/media_store_source.dart';
import 'package:aves/model/source/source_state.dart';
import 'package:aves/services/common/services.dart';
import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AnalysisService {

View file

@ -159,7 +159,7 @@ class PlatformMediaFileService implements MediaFileService {
int? pageId,
int? expectedContentLength,
BytesReceivedCallback? onBytesReceived,
}) {
}) async {
try {
final completer = Completer<Uint8List>.sync();
final sink = OutputBuffer();
@ -191,11 +191,12 @@ class PlatformMediaFileService implements MediaFileService {
},
cancelOnError: true,
);
return completer.future;
// `await` here, so that `completeError` will be caught below
return await completer.future;
} on PlatformException catch (e, stack) {
reportService.recordError(e, stack);
await reportService.recordError(e, stack);
}
return Future.sync(() => Uint8List(0));
return Uint8List(0);
}
@override

View file

@ -172,7 +172,8 @@ class PlatformStorageService implements StorageService {
},
cancelOnError: true,
);
return completer.future;
// `await` here, so that `completeError` will be caught below
return await completer.future;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -196,7 +197,8 @@ class PlatformStorageService implements StorageService {
},
cancelOnError: true,
);
return completer.future;
// `await` here, so that `completeError` will be caught below
return await completer.future;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -220,7 +222,8 @@ class PlatformStorageService implements StorageService {
},
cancelOnError: true,
);
return completer.future;
// `await` here, so that `completeError` will be caught below
return await completer.future;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
@ -247,7 +250,8 @@ class PlatformStorageService implements StorageService {
},
cancelOnError: true,
);
return completer.future;
// `await` here, so that `completeError` will be caught below
return await completer.future;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}

View file

@ -626,6 +626,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
final name = result.item2;
if (name.isEmpty) return;
unawaited(androidAppService.pinToHomeScreen(name, coverEntry, filters: filters));
await androidAppService.pinToHomeScreen(name, coverEntry, filters: filters);
if (!device.showPinShortcutFeedback) {
showFeedback(context, context.l10n.genericSuccessFeedback);
}
}
}

View file

@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.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/filters/album.dart';
import 'package:aves/model/highlight.dart';
@ -124,7 +125,10 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
final name = result.item2;
if (name.isEmpty) return;
unawaited(androidAppService.pinToHomeScreen(name, entry, uri: entry.uri));
await androidAppService.pinToHomeScreen(name, entry, uri: entry.uri);
if (!device.showPinShortcutFeedback) {
showFeedback(context, context.l10n.genericSuccessFeedback);
}
}
Future<void> _flip(BuildContext context, AvesEntry entry) async {

View file

@ -88,6 +88,7 @@ class ViewerTopOverlay extends StatelessWidget {
case EntryAction.rotateScreen:
return settings.isRotationLocked;
case EntryAction.addShortcut:
return device.canPinShortcut;
case EntryAction.copyToClipboard:
case EntryAction.edit:
case EntryAction.info:

View file

@ -1025,7 +1025,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: d644fedd9cb79a45b1b92788880e81b846a69d9b
resolved-ref: fba50f0e380d8cbd6a5bbda32f97a9c5e4d033e2
url: "git://github.com/deckerst/aves_streams_channel.git"
source: git
version: "0.3.0"
@ -1203,7 +1203,7 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.0"
version: "2.3.1"
wkt_parser:
dependency: transitive
description: