fixed selecting settings file to import on older devices

This commit is contained in:
Thibault Deckers 2021-12-10 17:45:38 +09:00
parent 8cb88cc12c
commit eb3a8f5626
11 changed files with 40 additions and 33 deletions

View file

@ -155,7 +155,7 @@ class MainActivity : FlutterActivity() {
} }
} }
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant", "ObsoleteSdkInt")
private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) { private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) {
val treeUri = data?.data val treeUri = data?.data
if (resultCode != RESULT_OK || treeUri == null) { if (resultCode != RESULT_OK || treeUri == null) {

View file

@ -272,13 +272,13 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} else { } else {
var mimeType = "*/*" var mimeType = "*/*"
if (mimeTypes.size == 1) { if (mimeTypes.size == 1) {
// items have the same mime type & subtype // items have the same MIME type & subtype
mimeType = mimeTypes.first() mimeType = mimeTypes.first()
} else { } else {
// items have different subtypes // items have different subtypes
val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct() val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct()
if (mimeTypeTypes.size == 1) { if (mimeTypeTypes.size == 1) {
// items have the same mime type // items have the same MIME type
mimeType = "${mimeTypeTypes.first()}/*" mimeType = "${mimeTypeTypes.first()}/*"
} }
} }

View file

@ -411,8 +411,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// File type // File type
for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) { for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) {
// * `metadata-extractor` sometimes detects the wrong mime type (e.g. `pef` file as `tiff`, `mpeg` as `dvd`) // * `metadata-extractor` sometimes detects the wrong MIME type (e.g. `pef` file as `tiff`, `mpeg` as `dvd`)
// * the content resolver / media store sometimes reports the wrong mime type (e.g. `png` file as `jpeg`, `tiff` as `srw`) // * the content resolver / media store sometimes reports the wrong MIME type (e.g. `png` file as `jpeg`, `tiff` as `srw`)
// * `context.getContentResolver().getType()` sometimes returns an incorrect value // * `context.getContentResolver().getType()` sometimes returns an incorrect value
// * `MediaMetadataRetriever.setDataSource()` sometimes fails with `status = 0x80000000` // * `MediaMetadataRetriever.setDataSource()` sometimes fails with `status = 0x80000000`
// * file extension is unreliable // * file extension is unreliable

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.streams package deckers.thibault.aves.channel.streams
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -10,6 +11,7 @@ import android.util.Log
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.PendingStorageAccessResultHandler import deckers.thibault.aves.PendingStorageAccessResultHandler
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.PermissionManager import deckers.thibault.aves.utils.PermissionManager
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink import io.flutter.plugin.common.EventChannel.EventSink
@ -90,9 +92,9 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
endOfStream() endOfStream()
} }
@SuppressLint("ObsoleteSdkInt")
private fun createFile() { private fun createFile() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
// TODO TLAD [<=API18] create file
error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
return return
} }
@ -133,24 +135,16 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
} }
private fun openFile() { @SuppressLint("ObsoleteSdkInt")
private suspend fun openFile() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
// TODO TLAD [<=API18] open file
error("openFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) error("openFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
return return
} }
val mimeType = args["mimeType"] as String? val mimeType = args["mimeType"] as String? // optional
if (mimeType == null) {
error("openFile-args", "failed because of missing arguments", null)
return
}
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { fun onGranted(uri: Uri) {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
}
MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, { uri ->
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
activity.contentResolver.openInputStream(uri)?.use { input -> activity.contentResolver.openInputStream(uri)?.use { input ->
val buffer = ByteArray(BUFFER_SIZE) val buffer = ByteArray(BUFFER_SIZE)
@ -161,11 +155,24 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
endOfStream() endOfStream()
} }
} }
}, { }
fun onDenied() {
success(ByteArray(0)) success(ByteArray(0))
endOfStream() endOfStream()
}) }
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
setTypeAndNormalize(mimeType ?: MimeTypes.ANY)
}
if (intent.resolveActivity(activity.packageManager) != null) {
MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, ::onGranted, ::onDenied)
activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST) activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST)
} else {
MainActivity.notifyError("failed to resolve activity for intent=$intent")
onDenied()
}
} }
override fun onCancel(arguments: Any?) {} override fun onCancel(arguments: Any?) {}

View file

@ -161,7 +161,7 @@ class SourceEntry {
Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input) val metadata = ImageMetadataReader.readMetadata(input)
// do not switch on specific mime types, as the reported mime type could be wrong // do not switch on specific MIME types, as the reported MIME type could be wrong
// (e.g. PNG registered as JPG) // (e.g. PNG registered as JPG)
if (isVideo) { if (isVideo) {
for (dir in metadata.getDirectoriesOfType(AviDirectory::class.java)) { for (dir in metadata.getDirectoriesOfType(AviDirectory::class.java)) {

View file

@ -175,7 +175,7 @@ class MediaStoreImageProvider : ImageProvider() {
// but for single items, `contentUri` already contains the ID // but for single items, `contentUri` already contains the ID
val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, contentId.toLong()) val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, contentId.toLong())
// `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices) // `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices)
// in that case we try to use the mime type provided along the URI // in that case we try to use the MIME type provided along the URI
val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType
val width = cursor.getInt(widthColumn) val width = cursor.getInt(widthColumn)
val height = cursor.getInt(heightColumn) val height = cursor.getInt(heightColumn)

View file

@ -3,7 +3,7 @@ package deckers.thibault.aves.utils
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
object MimeTypes { object MimeTypes {
private const val IMAGE = "image" const val ANY = "*/*";
// generic raster // generic raster
const val BMP = "image/bmp" const val BMP = "image/bmp"
@ -45,8 +45,6 @@ object MimeTypes {
// vector // vector
const val SVG = "image/svg+xml" const val SVG = "image/svg+xml"
private const val VIDEO = "video"
private const val AVI = "video/avi" private const val AVI = "video/avi"
private const val AVI_VND = "video/vnd.avi" private const val AVI_VND = "video/vnd.avi"
const val DVD = "video/dvd" const val DVD = "video/dvd"
@ -58,9 +56,9 @@ object MimeTypes {
private const val OGV = "video/ogg" private const val OGV = "video/ogg"
private const val WEBM = "video/webm" private const val WEBM = "video/webm"
fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith(IMAGE) fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith("image")
fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith(VIDEO) fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith("video")
fun isHeic(mimeType: String?) = mimeType != null && (mimeType == HEIC || mimeType == HEIF) fun isHeic(mimeType: String?) = mimeType != null && (mimeType == HEIC || mimeType == HEIF)

View file

@ -144,7 +144,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override @override
Future<bool> shareEntries(Iterable<AvesEntry> entries) async { Future<bool> shareEntries(Iterable<AvesEntry> entries) async {
// loosen mime type to a generic one, so we can share with badly defined apps // loosen MIME type to a generic one, so we can share with badly defined apps
// e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats // e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats
final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
try { try {

View file

@ -133,7 +133,7 @@ class AndroidDebugService {
static Future<Map> getMetadataExtractorSummary(AvesEntry entry) async { static Future<Map> getMetadataExtractorSummary(AvesEntry entry) async {
try { try {
// returns map with the mime type and tag count for each directory found by `metadata-extractor` // returns map with the MIME type and tag count for each directory found by `metadata-extractor`
final result = await platform.invokeMethod('getMetadataExtractorSummary', <String, dynamic>{ final result = await platform.invokeMethod('getMetadataExtractorSummary', <String, dynamic>{
'mimeType': entry.mimeType, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,

View file

@ -36,7 +36,7 @@ abstract class StorageService {
// return whether operation succeeded (`null` if user cancelled) // return whether operation succeeded (`null` if user cancelled)
Future<bool?> createFile(String name, String mimeType, Uint8List bytes); Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
Future<Uint8List> openFile(String mimeType); Future<Uint8List> openFile([String? mimeType]);
} }
class PlatformStorageService implements StorageService { class PlatformStorageService implements StorageService {
@ -231,7 +231,7 @@ class PlatformStorageService implements StorageService {
} }
@override @override
Future<Uint8List> openFile(String mimeType) async { Future<Uint8List> openFile([String? mimeType]) async {
try { try {
final completer = Completer<Uint8List>.sync(); final completer = Completer<Uint8List>.sync();
final sink = OutputBuffer(); final sink = OutputBuffer();

View file

@ -123,7 +123,9 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
} }
break; break;
case SettingsAction.import: case SettingsAction.import:
final bytes = await storageService.openFile(MimeTypes.json); // specifying the JSON MIME type to restrict openable files is correct in theory,
// but older devices (e.g. SM-P580, API 27) that do not recognize JSON files as such would filter them out
final bytes = await storageService.openFile();
if (bytes.isNotEmpty) { if (bytes.isNotEmpty) {
try { try {
await settings.fromJson(utf8.decode(bytes)); await settings.fromJson(utf8.decode(bytes));