API 16 support prep
This commit is contained in:
parent
089304da2d
commit
35958d87fd
20 changed files with 143 additions and 91 deletions
|
@ -56,8 +56,7 @@ android {
|
||||||
// minSdkVersion constraints:
|
// minSdkVersion constraints:
|
||||||
// - Flutter & other plugins: 16
|
// - Flutter & other plugins: 16
|
||||||
// - google_maps_flutter v2.1.1: 20
|
// - google_maps_flutter v2.1.1: 20
|
||||||
// - Aves native: 19
|
minSdkVersion 16
|
||||||
minSdkVersion 19
|
|
||||||
targetSdkVersion 31
|
targetSdkVersion 31
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
@ -149,7 +148,7 @@ dependencies {
|
||||||
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
||||||
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a'
|
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a'
|
||||||
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android
|
// 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'
|
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||||
|
|
||||||
kapt 'androidx.annotation:annotation:1.3.0'
|
kapt 'androidx.annotation:annotation:1.3.0'
|
||||||
|
|
|
@ -23,7 +23,6 @@ import io.flutter.embedding.engine.FlutterEngine
|
||||||
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 kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
||||||
private var backgroundFlutterEngine: FlutterEngine? = null
|
private var backgroundFlutterEngine: FlutterEngine? = null
|
||||||
|
@ -141,11 +140,12 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
||||||
getString(R.string.analysis_notification_action_stop),
|
getString(R.string.analysis_notification_action_stop),
|
||||||
stopServiceIntent
|
stopServiceIntent
|
||||||
).build()
|
).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)
|
return NotificationCompat.Builder(this, CHANNEL_ANALYSIS)
|
||||||
.setContentTitle(title ?: getText(R.string.analysis_notification_default_title))
|
.setContentTitle(title ?: getText(R.string.analysis_notification_default_title))
|
||||||
.setContentText(message)
|
.setContentText(message)
|
||||||
.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
|
.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(icon)
|
||||||
.setContentIntent(openAppIntent)
|
.setContentIntent(openAppIntent)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.addAction(stopAction)
|
.addAction(stopAction)
|
||||||
|
|
|
@ -24,10 +24,12 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
|
|
||||||
private fun areAnimationsRemoved(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
private fun areAnimationsRemoved(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||||
var removed = false
|
var removed = false
|
||||||
try {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f
|
try {
|
||||||
} catch (e: Exception) {
|
removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f
|
||||||
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.success(removed)
|
result.success(removed)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,14 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
fun addPackageDetails(intent: Intent) {
|
fun addPackageDetails(intent: Intent) {
|
||||||
// apps tend to use their name in English when creating directories
|
// apps tend to use their name in English when creating directories
|
||||||
// so we get their names in English as well as the current locale
|
// 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
|
val pm = context.packageManager
|
||||||
for (resolveInfo in pm.queryIntentActivities(intent, 0)) {
|
for (resolveInfo in pm.queryIntentActivities(intent, 0)) {
|
||||||
|
|
|
@ -20,15 +20,21 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCapabilities(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
private fun getCapabilities(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||||
result.success(hashMapOf(
|
val sdkInt = Build.VERSION.SDK_INT
|
||||||
"canGrantDirectoryAccess" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP),
|
result.success(
|
||||||
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
|
hashMapOf(
|
||||||
"canPrint" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT),
|
"canGrantDirectoryAccess" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
|
||||||
// as of google_maps_flutter v2.1.1, minSDK is 20 because of default PlatformView usage,
|
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
|
||||||
// but using hybrid composition would make it usable on API 19 too,
|
"canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT),
|
||||||
// cf https://github.com/flutter/flutter/issues/23728
|
"canRenderEmojis" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
|
||||||
"canRenderGoogleMaps" to (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH),
|
// 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) {
|
private fun getDefaultTimeZone(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
|
|
@ -584,7 +584,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
|
|
||||||
var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
|
var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
|
||||||
try {
|
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)) {
|
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
|
||||||
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { metadataMap[KEY_DATE_MILLIS] = it }
|
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { metadataMap[KEY_DATE_MILLIS] = it }
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.streams
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
@ -32,12 +33,13 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
|
||||||
|
|
||||||
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||||
if (update()) {
|
if (update()) {
|
||||||
success(
|
val settings: FieldMap = hashMapOf(
|
||||||
hashMapOf(
|
Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
|
||||||
Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
|
|
||||||
Settings.Global.TRANSITION_ANIMATION_SCALE to transitionAnimationScale,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
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
|
accelerometerRotation = newAccelerometerRotation
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
if (transitionAnimationScale != newTransitionAnimationScale) {
|
val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE)
|
||||||
transitionAnimationScale = newTransitionAnimationScale
|
if (transitionAnimationScale != newTransitionAnimationScale) {
|
||||||
changed = true
|
transitionAnimationScale = newTransitionAnimationScale
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
|
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,11 +27,13 @@ object MediaMetadataRetrieverHelper {
|
||||||
MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS to "Number of Tracks",
|
MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS to "Number of Tracks",
|
||||||
MediaMetadataRetriever.METADATA_KEY_TITLE to "Title",
|
MediaMetadataRetriever.METADATA_KEY_TITLE to "Title",
|
||||||
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT to "Video Height",
|
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_VIDEO_WIDTH to "Video Width",
|
||||||
MediaMetadataRetriever.METADATA_KEY_WRITER to "Writer",
|
MediaMetadataRetriever.METADATA_KEY_WRITER to "Writer",
|
||||||
MediaMetadataRetriever.METADATA_KEY_YEAR to "Year",
|
MediaMetadataRetriever.METADATA_KEY_YEAR to "Year",
|
||||||
).apply {
|
).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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
put(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE, "Capture Framerate")
|
put(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE, "Capture Framerate")
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.drew.imaging.ImageMetadataReader
|
import com.drew.imaging.ImageMetadataReader
|
||||||
import com.drew.metadata.avi.AviDirectory
|
import com.drew.metadata.avi.AviDirectory
|
||||||
|
@ -135,10 +136,12 @@ class SourceEntry {
|
||||||
try {
|
try {
|
||||||
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) { width = it }
|
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) { width = it }
|
||||||
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) { height = 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.getSafeLong(MediaMetadataRetriever.METADATA_KEY_DURATION) { durationMillis = it }
|
||||||
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { sourceDateTakenMillis = it }
|
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { sourceDateTakenMillis = it }
|
||||||
retriever.getSafeString(MediaMetadataRetriever.METADATA_KEY_TITLE) { title = 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) {
|
} catch (e: Exception) {
|
||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -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 {
|
fun canInsertByMediaStore(directories: List<FieldMap>): Boolean {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
directories.all {
|
directories.all {
|
||||||
|
@ -217,14 +184,46 @@ object PermissionManager {
|
||||||
// from API 30 / Android 11 / R, any storage requires access permission
|
// from API 30 / Android 11 / R, any storage requires access permission
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
accessibleDirs.addAll(StorageUtils.getVolumePaths(context))
|
accessibleDirs.addAll(StorageUtils.getVolumePaths(context))
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
} else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||||
&& Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q
|
|
||||||
) {
|
|
||||||
accessibleDirs.add(StorageUtils.getPrimaryVolumePath(context))
|
accessibleDirs.add(StorageUtils.getPrimaryVolumePath(context))
|
||||||
}
|
}
|
||||||
return accessibleDirs
|
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
|
// As of Android R, `MediaStore.getDocumentUri` fails if any of the persisted
|
||||||
// URI permissions we hold points to a folder that no longer exists,
|
// URI permissions we hold points to a folder that no longer exists,
|
||||||
// so we should remove these obsolete URIs before proceeding.
|
// so we should remove these obsolete URIs before proceeding.
|
||||||
|
|
|
@ -5,7 +5,8 @@ final Device device = Device._private();
|
||||||
|
|
||||||
class Device {
|
class Device {
|
||||||
late final String _userAgent;
|
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;
|
String get userAgent => _userAgent;
|
||||||
|
|
||||||
|
@ -15,8 +16,15 @@ class Device {
|
||||||
|
|
||||||
bool get canPrint => _canPrint;
|
bool get canPrint => _canPrint;
|
||||||
|
|
||||||
|
bool get canRenderEmojis => _canRenderEmojis;
|
||||||
|
|
||||||
bool get canRenderGoogleMaps => _canRenderGoogleMaps;
|
bool get canRenderGoogleMaps => _canRenderGoogleMaps;
|
||||||
|
|
||||||
|
// TODO TLAD toggle settings > import/export, about > bug report > save
|
||||||
|
bool get hasFilePicker => _hasFilePicker;
|
||||||
|
|
||||||
|
bool get showPinShortcutFeedback => _showPinShortcutFeedback;
|
||||||
|
|
||||||
Device._private();
|
Device._private();
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
@ -27,6 +35,9 @@ class Device {
|
||||||
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
|
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
|
||||||
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
|
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
|
||||||
_canPrint = capabilities['canPrint'] ?? false;
|
_canPrint = capabilities['canPrint'] ?? false;
|
||||||
|
_canRenderEmojis = capabilities['canRenderEmojis'] ?? false;
|
||||||
_canRenderGoogleMaps = capabilities['canRenderGoogleMaps'] ?? false;
|
_canRenderGoogleMaps = capabilities['canRenderGoogleMaps'] ?? false;
|
||||||
|
_hasFilePicker = capabilities['hasFilePicker'] ?? false;
|
||||||
|
_showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
||||||
];
|
];
|
||||||
|
|
||||||
static CollectionFilter? fromJson(String jsonString) {
|
static CollectionFilter? fromJson(String jsonString) {
|
||||||
|
if (jsonString.isEmpty) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final jsonMap = jsonDecode(jsonString);
|
final jsonMap = jsonDecode(jsonString);
|
||||||
if (jsonMap is Map<String, dynamic>) {
|
if (jsonMap is Map<String, dynamic>) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
@ -58,15 +59,17 @@ class LocationFilter extends CollectionFilter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) {
|
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) {
|
||||||
final flag = countryCodeToFlag(_countryCode);
|
if (_countryCode != null && device.canRenderEmojis) {
|
||||||
// as of Flutter v1.22.3, emoji shadows are rendered as colorful duplicates,
|
final flag = countryCodeToFlag(_countryCode);
|
||||||
// not filled with the shadow color as expected, so we remove them
|
// as of Flutter v1.22.3, emoji shadows are rendered as colorful duplicates,
|
||||||
if (flag != null) {
|
// not filled with the shadow color as expected, so we remove them
|
||||||
return Text(
|
if (flag != null) {
|
||||||
flag,
|
return Text(
|
||||||
style: TextStyle(fontSize: size, shadows: const []),
|
flag,
|
||||||
textScaleFactor: 1.0,
|
style: TextStyle(fontSize: size, shadows: const []),
|
||||||
);
|
textScaleFactor: 1.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Icon(_location.isEmpty ? AIcons.locationOff : AIcons.location, size: size);
|
return Icon(_location.isEmpty ? AIcons.locationOff : AIcons.location, size: size);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ import 'package:aves/model/source/media_store_source.dart';
|
||||||
import 'package:aves/model/source/source_state.dart';
|
import 'package:aves/model/source/source_state.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:fijkplayer/fijkplayer.dart';
|
import 'package:fijkplayer/fijkplayer.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
class AnalysisService {
|
class AnalysisService {
|
||||||
|
|
|
@ -159,7 +159,7 @@ class PlatformMediaFileService implements MediaFileService {
|
||||||
int? pageId,
|
int? pageId,
|
||||||
int? expectedContentLength,
|
int? expectedContentLength,
|
||||||
BytesReceivedCallback? onBytesReceived,
|
BytesReceivedCallback? onBytesReceived,
|
||||||
}) {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final completer = Completer<Uint8List>.sync();
|
final completer = Completer<Uint8List>.sync();
|
||||||
final sink = OutputBuffer();
|
final sink = OutputBuffer();
|
||||||
|
@ -191,11 +191,12 @@ class PlatformMediaFileService implements MediaFileService {
|
||||||
},
|
},
|
||||||
cancelOnError: true,
|
cancelOnError: true,
|
||||||
);
|
);
|
||||||
return completer.future;
|
// `await` here, so that `completeError` will be caught below
|
||||||
|
return await completer.future;
|
||||||
} on PlatformException catch (e, stack) {
|
} on PlatformException catch (e, stack) {
|
||||||
reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
}
|
}
|
||||||
return Future.sync(() => Uint8List(0));
|
return Uint8List(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -172,7 +172,8 @@ class PlatformStorageService implements StorageService {
|
||||||
},
|
},
|
||||||
cancelOnError: true,
|
cancelOnError: true,
|
||||||
);
|
);
|
||||||
return completer.future;
|
// `await` here, so that `completeError` will be caught below
|
||||||
|
return await completer.future;
|
||||||
} on PlatformException catch (e, stack) {
|
} on PlatformException catch (e, stack) {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
}
|
}
|
||||||
|
@ -196,7 +197,8 @@ class PlatformStorageService implements StorageService {
|
||||||
},
|
},
|
||||||
cancelOnError: true,
|
cancelOnError: true,
|
||||||
);
|
);
|
||||||
return completer.future;
|
// `await` here, so that `completeError` will be caught below
|
||||||
|
return await completer.future;
|
||||||
} on PlatformException catch (e, stack) {
|
} on PlatformException catch (e, stack) {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
}
|
}
|
||||||
|
@ -220,7 +222,8 @@ class PlatformStorageService implements StorageService {
|
||||||
},
|
},
|
||||||
cancelOnError: true,
|
cancelOnError: true,
|
||||||
);
|
);
|
||||||
return completer.future;
|
// `await` here, so that `completeError` will be caught below
|
||||||
|
return await completer.future;
|
||||||
} on PlatformException catch (e, stack) {
|
} on PlatformException catch (e, stack) {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
}
|
}
|
||||||
|
@ -247,7 +250,8 @@ class PlatformStorageService implements StorageService {
|
||||||
},
|
},
|
||||||
cancelOnError: true,
|
cancelOnError: true,
|
||||||
);
|
);
|
||||||
return completer.future;
|
// `await` here, so that `completeError` will be caught below
|
||||||
|
return await completer.future;
|
||||||
} on PlatformException catch (e, stack) {
|
} on PlatformException catch (e, stack) {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
}
|
}
|
||||||
|
|
|
@ -626,6 +626,9 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
||||||
final name = result.item2;
|
final name = result.item2;
|
||||||
if (name.isEmpty) return;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:convert';
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_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/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
|
@ -124,7 +125,10 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
final name = result.item2;
|
final name = result.item2;
|
||||||
if (name.isEmpty) return;
|
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 {
|
Future<void> _flip(BuildContext context, AvesEntry entry) async {
|
||||||
|
|
|
@ -88,6 +88,7 @@ class ViewerTopOverlay extends StatelessWidget {
|
||||||
case EntryAction.rotateScreen:
|
case EntryAction.rotateScreen:
|
||||||
return settings.isRotationLocked;
|
return settings.isRotationLocked;
|
||||||
case EntryAction.addShortcut:
|
case EntryAction.addShortcut:
|
||||||
|
return device.canPinShortcut;
|
||||||
case EntryAction.copyToClipboard:
|
case EntryAction.copyToClipboard:
|
||||||
case EntryAction.edit:
|
case EntryAction.edit:
|
||||||
case EntryAction.info:
|
case EntryAction.info:
|
||||||
|
|
|
@ -1025,7 +1025,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: d644fedd9cb79a45b1b92788880e81b846a69d9b
|
resolved-ref: fba50f0e380d8cbd6a5bbda32f97a9c5e4d033e2
|
||||||
url: "git://github.com/deckerst/aves_streams_channel.git"
|
url: "git://github.com/deckerst/aves_streams_channel.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.3.0"
|
version: "0.3.0"
|
||||||
|
@ -1203,7 +1203,7 @@ packages:
|
||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.1"
|
||||||
wkt_parser:
|
wkt_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
Loading…
Reference in a new issue