diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ff63c3db..b66683330 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [v1.3.5] - 2021-02-26
+### Added
+- support Android KitKat, Lollipop & Marshmallow (API 19 ~ 23)
+- quick country reverse geocoding without Play Services
+- menu option to hide any filter
+- menu option to navigate to the album / country / tag page from filter
+
+### Changed
+- analytics are opt-in
+
+### Removed
+- removed custom font used in titles and info page
+
## [v1.3.4] - 2021-02-10
### Added
- hide album / country / tag from collection
diff --git a/README.md b/README.md
index ba87c7bcd..81b930ab2 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
- search and filter by country, place, XMP tag, type (animated, raster, vector…)
- favorites
- statistics
-- support Android API 24 ~ 30 (Nougat ~ R)
+- support Android API 19 ~ 30 (KitKat ~ R)
- Android integration (app shortcuts, handle view/pick intents)
## Known Issues
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 8121a9d6a..5247b3a54 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -53,8 +53,7 @@ android {
defaultConfig {
applicationId "deckers.thibault.aves"
- // TODO TLAD try minSdkVersion 23
- minSdkVersion 24
+ minSdkVersion 19
targetSdkVersion 30 // same as compileSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 66f30eaf6..ea311e8df 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -14,10 +14,6 @@
https://developer.android.com/preview/privacy/storage#media-file-access
- raw path access:
https://developer.android.com/preview/privacy/storage#media-files-raw-paths
-
- Android R issues:
- - users cannot grant directory access to the root Downloads directory,
- - users cannot grant directory access to the root directory of each reliable SD card volume
-->
@@ -31,9 +27,7 @@
-
+
@@ -48,6 +42,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true">
+
ImageByteStreamHandler(this, args) }
StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageOpStreamHandler(this, args) }
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
index a813d1943..9df5b08c6 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
@@ -4,7 +4,6 @@ import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
import android.util.Log
@@ -89,7 +88,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
@Suppress("DEPRECATION")
resources.updateConfiguration(englishConfig, resources.displayMetrics)
englishLabel = resources.getString(labelRes)
- } catch (e: PackageManager.NameNotFoundException) {
+ } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e)
}
englishLabel
@@ -145,7 +144,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e)
}
Glide.with(context).clear(target)
- } catch (e: PackageManager.NameNotFoundException) {
+ } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get app info for packageName=$packageName", e)
return
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
index 0de4bf39f..cea3fe155 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
@@ -50,14 +50,23 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
}
private fun getContextDirs() = hashMapOf(
- "dataDir" to context.dataDir,
"cacheDir" to context.cacheDir,
- "codeCacheDir" to context.codeCacheDir,
"filesDir" to context.filesDir,
- "noBackupFilesDir" to context.noBackupFilesDir,
"obbDir" to context.obbDir,
"externalCacheDir" to context.externalCacheDir,
- ).mapValues { it.value?.path }
+ ).apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ putAll(
+ hashMapOf(
+ "codeCacheDir" to context.codeCacheDir,
+ "noBackupFilesDir" to context.noBackupFilesDir,
+ )
+ )
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ put("dataDir", context.dataDir)
+ }
+ }.mapValues { it.value?.path }
private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument("uri")?.let { Uri.parse(it) }
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt
index 5af8a45f3..67f6afec8 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt
@@ -125,7 +125,8 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
pageId = pageId,
sampleSize = sampleSize,
regionRect = regionRect,
- imageSize = Size(imageWidth, imageHeight),
+ imageWidth = imageWidth,
+ imageHeight = imageHeight,
result = result,
)
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt
index d0e315d08..d35b00134 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt
@@ -119,7 +119,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
// optional parent to distinguish child directories of the same type
dir.parent?.name?.let { dirName = "$it/$dirName" }
- val dirMap = metadataMap.getOrDefault(dirName, HashMap())
+ val dirMap = metadataMap[dirName] ?: HashMap()
metadataMap[dirName] = dirMap
// tags
@@ -325,7 +325,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
if (xmpMeta.doesPropertyExist(XMP.DC_SCHEMA_NS, XMP.SUBJECT_PROP_NAME)) {
val count = xmpMeta.countArrayItems(XMP.DC_SCHEMA_NS, XMP.SUBJECT_PROP_NAME)
val values = (1 until count + 1).map { xmpMeta.getArrayItem(XMP.DC_SCHEMA_NS, XMP.SUBJECT_PROP_NAME, it).value }
- metadataMap[KEY_XMP_SUBJECTS] = values.joinToString(separator = XMP_SUBJECTS_SEPARATOR)
+ metadataMap[KEY_XMP_SUBJECTS] = values.joinToString(XMP_SUBJECTS_SEPARATOR)
}
xmpMeta.getSafeLocalizedText(XMP.DC_SCHEMA_NS, XMP.TITLE_PROP_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE_DESCRIPTION] = it }
if (!metadataMap.containsKey(KEY_XMP_TITLE_DESCRIPTION)) {
@@ -350,7 +350,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
// XMP fallback to IPTC
if (!metadataMap.containsKey(KEY_XMP_SUBJECTS)) {
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
- dir.keywords?.let { metadataMap[KEY_XMP_SUBJECTS] = it.joinToString(separator = XMP_SUBJECTS_SEPARATOR) }
+ dir.keywords?.let { metadataMap[KEY_XMP_SUBJECTS] = it.joinToString(XMP_SUBJECTS_SEPARATOR) }
}
}
@@ -594,7 +594,9 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
KEY_MIME_TYPE to trackMime,
)
format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { page[KEY_IS_DEFAULT] = it != 0 }
- format.getSafeInt(MediaFormat.KEY_TRACK_ID) { page[KEY_TRACK_ID] = it }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ format.getSafeInt(MediaFormat.KEY_TRACK_ID) { page[KEY_TRACK_ID] = it }
+ }
format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it }
format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it }
if (isVideo(trackMime)) {
@@ -677,26 +679,35 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
val projection = arrayOf(prop)
- val cursor = context.contentResolver.query(contentUri, projection, null, null, null)
- if (cursor != null && cursor.moveToFirst()) {
- var value: Any? = null
- try {
- value = when (cursor.getType(0)) {
- Cursor.FIELD_TYPE_NULL -> null
- Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0)
- Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0)
- Cursor.FIELD_TYPE_STRING -> cursor.getString(0)
- Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0)
- else -> null
- }
- } catch (e: Exception) {
- Log.w(LOG_TAG, "failed to get value for key=$prop", e)
- }
- cursor.close()
- result.success(value?.toString())
- } else {
- result.error("getContentResolverProp-null", "failed to get cursor for contentUri=$contentUri", null)
+ val cursor: Cursor?
+ try {
+ cursor = context.contentResolver.query(contentUri, projection, null, null, null)
+ } catch (e: Exception) {
+ // throws SQLiteException when the requested prop is not a known column
+ result.error("getContentResolverProp-query", "failed to query for contentUri=$contentUri", e.message)
+ return
}
+
+ if (cursor == null || !cursor.moveToFirst()) {
+ result.error("getContentResolverProp-cursor", "failed to get cursor for contentUri=$contentUri", null)
+ return
+ }
+
+ var value: Any? = null
+ try {
+ value = when (cursor.getType(0)) {
+ Cursor.FIELD_TYPE_NULL -> null
+ Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0)
+ Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0)
+ Cursor.FIELD_TYPE_STRING -> cursor.getString(0)
+ Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0)
+ else -> null
+ }
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "failed to get value for key=$prop", e)
+ }
+ cursor.close()
+ result.success(value?.toString())
}
private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
index 248db4ff6..e7cca3ea5 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt
@@ -4,9 +4,12 @@ import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
+import android.os.Environment
import android.os.storage.StorageManager
+import androidx.core.os.EnvironmentCompat
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.utils.PermissionManager
+import deckers.thibault.aves.utils.StorageUtils.getPrimaryVolumePath
import deckers.thibault.aves.utils.StorageUtils.getVolumePaths
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@@ -24,6 +27,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
"getFreeSpace" -> safe(call, result, ::getFreeSpace)
"getGrantedDirectories" -> safe(call, result, ::getGrantedDirectories)
"getInaccessibleDirectories" -> safe(call, result, ::getInaccessibleDirectories)
+ "getRestrictedDirectories" -> safe(call, result, ::getRestrictedDirectories)
"revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess)
"scanFile" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::scanFile) }
else -> result.notImplemented()
@@ -31,31 +35,52 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
}
private fun getStorageVolumes(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
- val volumes: List