support Android KitKat API 19-20
This commit is contained in:
parent
9173ee9121
commit
cadd2b4d1c
14 changed files with 96 additions and 37 deletions
|
@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- support Android Lollipop & Marshmallow (API 21 ~ 23)
|
||||
- support Android KitKat, Lollipop & Marshmallow (API 19 ~ 23)
|
||||
|
||||
## [v1.3.4] - 2021-02-10
|
||||
### Added
|
||||
|
|
|
@ -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 21 ~ 30 (Lollipop ~ R)
|
||||
- support Android API 19 ~ 30 (KitKat ~ R)
|
||||
- Android integration (app shortcuts, handle view/pick intents)
|
||||
|
||||
## Known Issues
|
||||
|
|
|
@ -53,7 +53,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "deckers.thibault.aves"
|
||||
minSdkVersion 21
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 30 // same as compileSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
|
|
@ -51,13 +51,21 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
|
|||
|
||||
private fun getContextDirs() = hashMapOf(
|
||||
"cacheDir" to context.cacheDir,
|
||||
"codeCacheDir" to context.codeCacheDir,
|
||||
"filesDir" to context.filesDir,
|
||||
"noBackupFilesDir" to context.noBackupFilesDir,
|
||||
"obbDir" to context.obbDir,
|
||||
"externalCacheDir" to context.externalCacheDir,
|
||||
).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) put("dataDir", context.dataDir)
|
||||
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) {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ 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
|
||||
|
@ -61,12 +62,19 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
for (volumePath in getVolumePaths(context)) {
|
||||
val volumeFile = File(volumePath)
|
||||
try {
|
||||
val isPrimary = volumePath == primaryVolumePath
|
||||
val isRemovable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Environment.isExternalStorageRemovable(volumeFile)
|
||||
} else {
|
||||
// random guess
|
||||
!isPrimary
|
||||
}
|
||||
volumes.add(
|
||||
hashMapOf(
|
||||
"path" to volumePath,
|
||||
"isPrimary" to (volumePath == primaryVolumePath),
|
||||
"isRemovable" to Environment.isExternalStorageRemovable(volumeFile),
|
||||
"state" to Environment.getExternalStorageState(volumeFile)
|
||||
"isPrimary" to isPrimary,
|
||||
"isRemovable" to isRemovable,
|
||||
"state" to EnvironmentCompat.getStorageState(volumeFile)
|
||||
)
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
|
@ -119,6 +127,11 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
result.error("revokeDirectoryAccess-unsupported", "volume access is not allowed before Android Lollipop", null)
|
||||
return
|
||||
}
|
||||
|
||||
val success = PermissionManager.revokeDirectoryAccess(context, path)
|
||||
result.success(success)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import io.flutter.plugin.common.MethodChannel
|
|||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import java.util.*
|
||||
|
||||
class TimeHandler() : MethodCallHandler {
|
||||
class TimeHandler : MethodCallHandler {
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"getDefaultTimeZone" -> result.success(TimeZone.getDefault().id)
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.graphics.BitmapFactory
|
|||
import android.graphics.BitmapRegionDecoder
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.util.Size
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
|
@ -37,7 +36,8 @@ class RegionFetcher internal constructor(
|
|||
pageId: Int?,
|
||||
sampleSize: Int,
|
||||
regionRect: Rect,
|
||||
imageSize: Size,
|
||||
imageWidth: Int,
|
||||
imageHeight: Int,
|
||||
result: MethodChannel.Result,
|
||||
) {
|
||||
if (MimeTypes.isHeifLike(mimeType) && pageId != null) {
|
||||
|
@ -48,7 +48,8 @@ class RegionFetcher internal constructor(
|
|||
pageId = null,
|
||||
sampleSize = sampleSize,
|
||||
regionRect = regionRect,
|
||||
imageSize = imageSize,
|
||||
imageWidth = imageWidth,
|
||||
imageHeight = imageHeight,
|
||||
result = result,
|
||||
)
|
||||
return
|
||||
|
@ -79,9 +80,9 @@ class RegionFetcher internal constructor(
|
|||
|
||||
// with raw images, the known image size may not match the decoded image size
|
||||
// so we scale the requested region accordingly
|
||||
val effectiveRect = if (imageSize.width != decoder.width || imageSize.height != decoder.height) {
|
||||
val xf = decoder.width.toDouble() / imageSize.width
|
||||
val yf = decoder.height.toDouble() / imageSize.height
|
||||
val effectiveRect = if (imageWidth != decoder.width || imageHeight != decoder.height) {
|
||||
val xf = decoder.width.toDouble() / imageWidth
|
||||
val yf = decoder.height.toDouble() / imageHeight
|
||||
Rect(
|
||||
(regionRect.left * xf).roundToInt(),
|
||||
(regionRect.top * yf).roundToInt(),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package deckers.thibault.aves.channel.streams
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
|
@ -26,6 +27,16 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
this.eventSink = eventSink
|
||||
handler = Handler(Looper.getMainLooper())
|
||||
|
||||
if (path == null) {
|
||||
error("requestVolumeAccess-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
error("requestVolumeAccess-unsupported", "volume access is not allowed before Android Lollipop", null)
|
||||
return
|
||||
}
|
||||
|
||||
requestVolumeAccess(activity, path!!, { success(true) }, { success(false) })
|
||||
}
|
||||
|
||||
|
@ -42,6 +53,17 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
endOfStream()
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) {
|
||||
handler.post {
|
||||
try {
|
||||
eventSink.error(errorCode, errorMessage, errorDetails)
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to use event sink", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun endOfStream() {
|
||||
handler.post {
|
||||
try {
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.os.Build
|
|||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import deckers.thibault.aves.utils.StorageUtils.PathSegments
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
@ -22,6 +23,7 @@ object PermissionManager {
|
|||
// permission request code to pending runnable
|
||||
private val pendingPermissionMap = ConcurrentHashMap<Int, PendingPermissionHandler>()
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun requestVolumeAccess(activity: Activity, path: String, onGranted: () -> Unit, onDenied: () -> Unit) {
|
||||
Log.i(LOG_TAG, "request user to select and grant access permission to volume=$path")
|
||||
|
||||
|
@ -106,29 +108,39 @@ object PermissionManager {
|
|||
}
|
||||
|
||||
fun getRestrictedDirectories(context: Context): List<Map<String, String>> {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
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)
|
||||
ArrayList<Map<String, String>>().apply {
|
||||
addAll(volumePaths.map {
|
||||
dirs.addAll(volumePaths.map {
|
||||
hashMapOf(
|
||||
"volumePath" to it,
|
||||
"relativeDir" to "",
|
||||
)
|
||||
})
|
||||
addAll(volumePaths.map {
|
||||
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 "",
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// TODO TLAD add KitKat restriction (no SD card root access) if min version goes to API 19-20
|
||||
ArrayList()
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun revokeDirectoryAccess(context: Context, path: String): Boolean {
|
||||
return StorageUtils.convertDirPathToTreeUri(context, path)?.let {
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
|
|
|
@ -12,6 +12,7 @@ import android.provider.MediaStore
|
|||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.commonsware.cwac.document.DocumentFileCompat
|
||||
import deckers.thibault.aves.utils.PermissionManager.getGrantedDirForPath
|
||||
import java.io.File
|
||||
|
@ -246,6 +247,7 @@ object StorageUtils {
|
|||
// e.g.
|
||||
// /storage/emulated/0/ -> content://com.android.externalstorage.documents/tree/primary%3A
|
||||
// /storage/10F9-3F13/Pictures/ -> content://com.android.externalstorage.documents/tree/10F9-3F13%3APictures
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
fun convertDirPathToTreeUri(context: Context, dirPath: String): Uri? {
|
||||
val uuid = getVolumeUuidForTreeUri(context, dirPath)
|
||||
if (uuid != null) {
|
||||
|
@ -287,7 +289,7 @@ object StorageUtils {
|
|||
|
||||
fun getDocumentFile(context: Context, anyPath: String, mediaUri: Uri): DocumentFileCompat? {
|
||||
try {
|
||||
if (requireAccessPermission(context, anyPath)) {
|
||||
if (requireAccessPermission(context, anyPath) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// need a document URI (not a media content URI) to open a `DocumentFile` output stream
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isMediaStoreContentUri(mediaUri)) {
|
||||
// cleanest API to get it
|
||||
|
@ -311,7 +313,7 @@ object StorageUtils {
|
|||
// returns null if directory does not exist and could not be created
|
||||
fun createDirectoryIfAbsent(context: Context, dirPath: String): DocumentFileCompat? {
|
||||
val cleanDirPath = ensureTrailingSeparator(dirPath)
|
||||
return if (requireAccessPermission(context, cleanDirPath)) {
|
||||
return if (requireAccessPermission(context, cleanDirPath) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val grantedDir = getGrantedDirForPath(context, cleanDirPath) ?: return null
|
||||
val rootTreeUri = convertDirPathToTreeUri(context, grantedDir) ?: return null
|
||||
var parentFile: DocumentFileCompat? = DocumentFileCompat.fromTreeUri(context, rootTreeUri) ?: return null
|
||||
|
|
Loading…
Reference in a new issue