support Android 13

This commit is contained in:
Thibault Deckers 2022-06-11 15:39:06 +09:00
parent 188a52deb0
commit c6a5316570
26 changed files with 168 additions and 59 deletions

View file

@ -6,7 +6,9 @@ All notable changes to this project will be documented in this file.
### Added
- set wallpaper from any media
- optional dynamic accent color on Android 12+
- support Android 13 (API 33)
- Turkish translation (thanks metezd)
### Changed

View file

@ -41,7 +41,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 32
compileSdkVersion 33
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -57,7 +57,7 @@ android {
// which implementation `DocumentBuilderImpl` is provided by the OS and is not customizable on Android,
// but the implementation on API <19 is not robust enough and fails to build XMP documents
minSdkVersion 19
targetSdkVersion 32
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']]
@ -154,7 +154,7 @@ repositories {
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.exifinterface:exifinterface:1.3.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.caverock:androidsvg-aar:1.4'

View file

@ -6,11 +6,11 @@
Scoped storage on Android Q is inconvenient because users need to confirm edition on each individual file.
So we request `WRITE_EXTERNAL_STORAGE` until Q (29), and enable `requestLegacyExternalStorage`
-->
<!-- TODO TLAD [tiramisu] need notification permission? -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- TODO TLAD [tiramisu] READ_MEDIA_IMAGE, READ_MEDIA_VIDEO instead of READ_EXTERNAL_STORAGE? -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
@ -18,6 +18,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- to show foreground service progress via notification -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- to access media with original metadata with scoped storage (Android Q+) -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
@ -138,6 +142,7 @@
<intent-filter>
<action android:name="android.intent.action.ATTACH_DATA" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>

View file

@ -159,7 +159,7 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
COMMAND_START -> {
runBlocking {
FlutterUtils.runOnUiThread {
val entryIds = data.get(KEY_ENTRY_IDS)?.takeIf { it is IntArray }?.let { (it as IntArray).toList() }
val entryIds = data.getIntArray(KEY_ENTRY_IDS)?.toList()
backgroundChannel?.invokeMethod(
"start", hashMapOf(
"entryIds" to entryIds,

View file

@ -15,6 +15,7 @@ import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.streams.*
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
@ -215,7 +216,7 @@ class MainActivity : FlutterActivity() {
}
}
Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> {
(intent.data ?: (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri))?.let { uri ->
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(context)
return hashMapOf(

View file

@ -8,6 +8,7 @@ import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodChannel
@ -79,7 +80,7 @@ class WallpaperActivity : FlutterActivity() {
private fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
when (intent?.action) {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
(intent.data ?: (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri))?.let { uri ->
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(context)
return hashMapOf(

View file

@ -30,6 +30,8 @@ import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getApplicationInfoCompat
import deckers.thibault.aves.utils.queryIntentActivitiesCompat
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -77,7 +79,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
val pm = context.packageManager
for (resolveInfo in pm.queryIntentActivities(intent, 0)) {
for (resolveInfo in pm.queryIntentActivitiesCompat(intent, 0)) {
val appInfo = resolveInfo.activityInfo.applicationInfo
val packageName = appInfo.packageName
if (!packages.containsKey(packageName)) {
@ -149,7 +151,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val size = (sizeDip * density).roundToInt()
var data: ByteArray? = null
try {
val iconResourceId = context.packageManager.getApplicationInfo(packageName, 0).icon
val iconResourceId = context.packageManager.getApplicationInfoCompat(packageName, 0).icon
if (iconResourceId != Resources.ID_NULL) {
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)

View file

@ -1,12 +1,17 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
import android.location.Address
import android.location.Geocoder
import android.os.Build
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.io.IOException
import java.util.*
@ -48,36 +53,48 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
Geocoder(context)
}
val addresses = try {
geocoder!!.getFromLocation(latitude, longitude, maxResults) ?: ArrayList()
} catch (e: IOException) {
// `grpc failed`, etc.
result.error("getAddress-network", "failed to get address because of network issues", e.message)
return
} catch (e: Exception) {
result.error("getAddress-exception", "failed to get address", e.message)
return
fun processAddresses(addresses: List<Address>) {
if (addresses.isEmpty()) {
result.error("getAddress-empty", "failed to find any address for latitude=$latitude, longitude=$longitude", null)
} else {
val addressMapList: ArrayList<Map<String, String?>> = ArrayList(addresses.map { address ->
hashMapOf(
"addressLine" to (0..address.maxAddressLineIndex).joinToString(", ") { i -> address.getAddressLine(i) },
"adminArea" to address.adminArea,
"countryCode" to address.countryCode,
"countryName" to address.countryName,
"featureName" to address.featureName,
"locality" to address.locality,
"postalCode" to address.postalCode,
"subAdminArea" to address.subAdminArea,
"subLocality" to address.subLocality,
"subThoroughfare" to address.subThoroughfare,
"thoroughfare" to address.thoroughfare,
)
})
result.success(addressMapList)
}
}
if (addresses.isEmpty()) {
result.error("getAddress-empty", "failed to find any address for latitude=$latitude, longitude=$longitude", null)
} else {
val addressMapList: ArrayList<Map<String, String?>> = ArrayList(addresses.map { address ->
hashMapOf(
"addressLine" to (0..address.maxAddressLineIndex).joinToString(", ") { i -> address.getAddressLine(i) },
"adminArea" to address.adminArea,
"countryCode" to address.countryCode,
"countryName" to address.countryName,
"featureName" to address.featureName,
"locality" to address.locality,
"postalCode" to address.postalCode,
"subAdminArea" to address.subAdminArea,
"subLocality" to address.subLocality,
"subThoroughfare" to address.subThoroughfare,
"thoroughfare" to address.thoroughfare,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
geocoder!!.getFromLocation(latitude, longitude, maxResults, object : Geocoder.GeocodeListener {
override fun onGeocode(addresses: List<Address?>) = processAddresses(addresses.filterNotNull())
override fun onError(errorMessage: String?) {
result.error("getAddress-asyncerror", "failed to get address", errorMessage)
}
})
result.success(addressMapList)
} else {
try {
@Suppress("deprecation")
val addresses = geocoder!!.getFromLocation(latitude, longitude, maxResults) ?: ArrayList()
processAddresses(addresses)
} catch (e: IOException) {
// `grpc failed`, etc.
result.error("getAddress-network", "failed to get address because of network issues", e.message)
} catch (e: Exception) {
result.error("getAddress-exception", "failed to get address", e.message)
}
}
}

View file

@ -220,7 +220,7 @@ object ExifInterfaceHelper {
// initialize metadata-extractor directories that we will fill
// by tags converted from the ExifInterface attributes
// so that we can rely on metadata-extractor descriptions
val dirs = DirType.values().associate { Pair(it, it.createDirectory()) }
val dirs = DirType.values().associateWith { it.createDirectory() }
// exclude Exif directory when it only includes image size
val isUselessExif = fun(it: Map<String, String>): Boolean {

View file

@ -0,0 +1,37 @@
package deckers.thibault.aves.utils
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.os.Build
import android.os.Parcelable
inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelableExtra(name, T::class.java)
} else {
@Suppress("deprecation")
getParcelableExtra<Parcelable>(name) as? T
}
}
fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(flags.toLong()))
} else {
@Suppress("deprecation")
getApplicationInfo(packageName, flags)
}
}
fun PackageManager.queryIntentActivitiesCompat(intent: Intent, flags: Int): List<ResolveInfo> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(flags.toLong()))
} else {
@Suppress("deprecation")
queryIntentActivities(intent, flags)
}
}

View file

@ -0,0 +1,32 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="100">
<group
android:scaleX=".44"
android:scaleY=".44"
android:translateX="28"
android:translateY="30">
<path
android:pathData="M3.925,16.034 L60.825,72.933a2.421,2.421 0.001,0 0,3.423 0l10.604,-10.603a6.789,6.789 90.001,0 0,0 -9.601L34.066,11.942A8.264,8.264 22.5,0 0,28.222 9.522H6.623A3.815,3.815 112.5,0 0,3.925 16.034Z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:pathData="m36.36,65.907v28.743a2.557,2.557 22.5,0 0,4.364 1.808L53.817,83.364a6.172,6.172 90,0 0,0 -8.729L42.532,63.35a3.616,3.616 157.5,0 0,-6.172 2.557z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:pathData="M79.653,40.078V11.335A2.557,2.557 22.5,0 0,75.289 9.527L62.195,22.62a6.172,6.172 90,0 0,0 8.729l11.285,11.285a3.616,3.616 157.5,0 0,6.172 -2.557z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:pathData="M96.613,16.867 L89.085,9.339a1.917,1.917 157.5,0 0,-3.273 1.356v6.172a4.629,4.629 45,0 0,4.629 4.629h4.255a2.712,2.712 112.5,0 0,1.917 -4.629z"
android:strokeWidth="5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
</group>
</vector>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_mono" />
</adaptive-icon>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_mono" />
</adaptive-icon>

View file

@ -1,17 +1,17 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.6.21'
ext.kotlin_version = '1.7.0'
repositories {
google()
mavenCentral()
maven { url 'https://developer.huawei.com/repo/' }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.0'
classpath 'com.android.tools.build:gradle:7.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// GMS & Firebase Crashlytics (used by some flavors only)
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.0'
// HMS (used by some flavors only)
classpath 'com.huawei.agconnect:agcp:1.5.2.300'
}

View file

@ -2,4 +2,4 @@
<b>Navigation und Suche</b> ist ein wichtiger Bestandteil von <i>Aves</i>. Das Ziel besteht darin, dass Benutzer problemlos von Alben zu Fotos zu Tags zu Karten usw. wechseln können.
<i>Aves</i> lässt sich mit Android (von <b>API 19 bis 32</b>, d. h. von KitKat bis Android 12L) mit Funktionen wie <b>App-Verknüpfungen</b> und <b>globaler Suche</b> integrieren. Es funktioniert auch als <b>Medienbetrachter und -auswahl</b>.
<i>Aves</i> lässt sich mit Android (von <b>API 19 bis 33</b>, d. h. von KitKat bis Android 13) mit Funktionen wie <b>App-Verknüpfungen</b> und <b>globaler Suche</b> integrieren. Es funktioniert auch als <b>Medienbetrachter und -auswahl</b>.

View file

@ -2,4 +2,4 @@
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
<i>Aves</i> integrates with Android (from <b>API 19 to 32</b>, i.e. from KitKat to Android 12L) with features such as <b>app shortcuts</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
<i>Aves</i> integrates with Android (from <b>API 19 to 33</b>, i.e. from KitKat to Android 13) with features such as <b>app shortcuts</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.

View file

@ -2,4 +2,4 @@
La <b>navegación y búsqueda</b> son partes importantes de <i>Aves</i>. Su propósito es que los usuarios puedan fácimente ir de álbumes a fotos, etiquetas, mapas, etc.
<i>Aves</i> se integra con Android (desde <b>API 19 a 32</b>, por ej. desde KitKat hasta Android 12L) con características como <b>vínculos de aplicación</b> y manejo de <b>búsqueda global</b>. También funciona como un <b>visor y seleccionador multimedia</b>.
<i>Aves</i> se integra con Android (desde <b>API 19 a 33</b>, por ej. desde KitKat hasta Android 13) con características como <b>vínculos de aplicación</b> y manejo de <b>búsqueda global</b>. También funciona como un <b>visor y seleccionador multimedia</b>.

View file

@ -2,4 +2,4 @@
<b>Navigasi dan pencarian</b> merupakan bagian penting dari <i>Aves</i>. Tujuannya adalah agar pengguna dengan mudah mengalir dari album ke foto ke tag ke peta, dll.
<i>Aves</i> terintegrasi dengan Android (dari <b>API 19 ke 32</b>, yaitu dari KitKat ke Android 12L) dengan fitur-fitur seperti <b>pintasan aplikasi</b> dan <b>pencarian global</b> penanganan. Ini juga berfungsi sebagai <b>penampil dan pemilih media</b>.
<i>Aves</i> terintegrasi dengan Android (dari <b>API 19 ke 33</b>, yaitu dari KitKat ke Android 13) dengan fitur-fitur seperti <b>pintasan aplikasi</b> dan <b>pencarian global</b> penanganan. Ini juga berfungsi sebagai <b>penampil dan pemilih media</b>.

View file

@ -2,4 +2,4 @@
<b>Navigazione e ricerca</b> sono una parte importante di <i>Aves</i>. L'obiettivo è che gli utenti passino facilmente dagli album alle foto, ai tag, alle mappe, ecc.
<i>Aves</i> si integra con Android (da <b>API 19 a 32</b>, cioè da KitKat ad Android 12L) con caratteristiche come <b>collegamenti alle app</b> e la gestione della <b>ricerca globale</b>. Funziona anche come <b>visualizzazione e raccolta di media</b>.
<i>Aves</i> si integra con Android (da <b>API 19 a 33</b>, cioè da KitKat ad Android 13) con caratteristiche come <b>collegamenti alle app</b> e la gestione della <b>ricerca globale</b>. Funziona anche come <b>visualizzazione e raccolta di media</b>.

View file

@ -4,4 +4,4 @@
<b>ナビゲーションと検索</b>は、Avesの重要な部分です。アルバムから写真、タグ、地図などへ簡単に移動できます。
<i>Aves</i>は、<b>アプリショートカット</b>や<b>グローバル検索</b>などの機能を、Android<b>API 19から32まで</b>、つまりAndroid 4.4から12 Lまでと統合しています。また、<b>メディアビューワー</b>や<b>メディアピッカー</b>としても機能します。
<i>Aves</i>は、<b>アプリショートカット</b>や<b>グローバル検索</b>などの機能を、Android<b>API 19から33まで</b>、つまりAndroid 4.4から13 Lまでと統合しています。また、<b>メディアビューワー</b>や<b>メディアピッカー</b>としても機能します。

View file

@ -2,4 +2,4 @@
<b>Navegação e pesquisa</b> é uma parte importante do <i>Aves</i>. O objetivo é que os usuários fluam facilmente de álbuns para fotos, etiquetas, mapas, etc.
<i>Aves</i> integra com Android (de <b>API 19 para 32</b>, i.e. de KitKat para Android 12L) com recursos como <b>atalhos de apps</b> e <b>pesquisa global</b> manipulação. Também funciona como um <b>visualizador e selecionador de mídia</b>.
<i>Aves</i> integra com Android (de <b>API 19 para 33</b>, i.e. de KitKat para Android 13) com recursos como <b>atalhos de apps</b> e <b>pesquisa global</b> manipulação. Também funciona como um <b>visualizador e selecionador de mídia</b>.

View file

@ -2,4 +2,4 @@
<b>Gezinme ve arama</b> <i>Aves'in</i> önemli bir parçasıdır. Amaç, kullanıcıların albümlerden fotoğraflara, etiketlerden haritalara vb. kolayca geçmesini sağlamaktır.
<i>Aves</i>, <b>uygulama kısayolları</b> ve <b>global arama<b> işleme gibi özelliklerle Android (<b>API 19'dan 32'ye</b>, yani KitKat'tan Android 12L'ye kadar) ile entegre olur. Ayrıca bir <b>medya görüntüleyici ve alıcı</b> olarak da çalışır.
<i>Aves</i>, <b>uygulama kısayolları</b> ve <b>global arama<b> işleme gibi özelliklerle Android (<b>API 19'dan 33'ye</b>, yani KitKat'tan Android 13'ye kadar) ile entegre olur. Ayrıca bir <b>medya görüntüleyici ve alıcı</b> olarak da çalışır.

View file

@ -2,4 +2,4 @@
<b>导航与搜索</b>是 <i>Aves</i> 的核心功能之一,旨在帮助用户在相册、照片、标签、地图等之间轻松切换。
<i> Aves</i> 与 Android<b>API 19-32</b>,即从 KitKat 到 Android 12L)集成,具有<b>快捷方式</b>和<b>全局搜索</b>等功能。它还可用作<b>媒体查看器和选择器<b>。
<i> Aves</i> 与 Android<b>API 19-33</b>,即从 KitKat 到 Android 13)集成,具有<b>快捷方式</b>和<b>全局搜索</b>等功能。它还可用作<b>媒体查看器和选择器<b>。

View file

@ -416,6 +416,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
}
}
if (startAnalysisService) {
// TODO TLAD [tiramisu] explain foreground service and request POST_NOTIFICATIONS permission
await AnalysisService.startService(
force: force,
entryIds: entries?.map((entry) => entry.id).toList(),

View file

@ -785,11 +785,13 @@ packages:
source: hosted
version: "9.2.0"
permission_handler_android:
dependency: transitive
dependency: "direct overridden"
description:
name: permission_handler_android
url: "https://pub.dartlang.org"
source: hosted
path: permission_handler_android
ref: HEAD
resolved-ref: "279cf44656272c6b89c73b16097108f3c973c31f"
url: "https://github.com/deckerst/flutter-permission-handler"
source: git
version: "9.0.2+1"
permission_handler_apple:
dependency: transitive

View file

@ -84,6 +84,13 @@ dependencies:
url_launcher:
xml:
dependency_overrides:
# TODO TLAD as of 2022/06/11, latest version (v9.0.2+1) does not support Android 13 storage permissions
permission_handler_android:
git:
url: https://github.com/deckerst/flutter-permission-handler
path: permission_handler_android
dev_dependencies:
flutter_test:
sdk: flutter