diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index cf480441b..470d68f28 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -15,7 +15,7 @@ jobs:
- uses: subosito/flutter-action@v1
with:
channel: stable
- flutter-version: '1.22.1'
+ flutter-version: '1.22.2'
- name: Clone the repository.
uses: actions/checkout@v2
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 280202c25..7836d6835 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,7 @@ jobs:
- uses: subosito/flutter-action@v1
with:
channel: stable
- flutter-version: '1.22.1'
+ flutter-version: '1.22.2'
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
# https://issuetracker.google.com/issues/144111441
@@ -50,8 +50,8 @@ jobs:
echo "${{ secrets.KEY_JKS }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE
rm release.keystore.asc
- flutter build apk --bundle-sksl-path shaders_1.22.1.sksl.json
- flutter build appbundle --bundle-sksl-path shaders_1.22.1.sksl.json
+ flutter build apk --bundle-sksl-path shaders_1.22.2.sksl.json
+ flutter build appbundle --bundle-sksl-path shaders_1.22.2.sksl.json
rm $AVES_STORE_FILE
env:
AVES_STORE_FILE: ${{ github.workspace }}/key.jks
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 3d0d01350..e7ab475f7 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -1,3 +1,13 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+ id 'com.google.gms.google-services'
+ id 'com.google.firebase.crashlytics'
+}
+
+// Flutter properties
+
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@@ -5,31 +15,20 @@ if (localPropertiesFile.exists()) {
localProperties.load(reader)
}
}
-
-def flutterRoot = localProperties.getProperty('flutter.sdk')
-if (flutterRoot == null) {
- throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
-}
-
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
-if (flutterVersionCode == null) {
- flutterVersionCode = '1'
-}
-
def flutterVersionName = localProperties.getProperty('flutter.versionName')
-if (flutterVersionName == null) {
- flutterVersionName = '1.0'
-}
-
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
+def flutterRoot = localProperties.getProperty('flutter.sdk')
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+// Keys
+
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
// for release using credentials stored in a local file
- keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+ keystorePropertiesFile.withReader('UTF-8') { reader ->
+ keystoreProperties.load(reader)
+ }
} else {
// for release using credentials in environment variables set up by Github Actions
// warning: in property file, single quotes should be escaped with a backslash
@@ -42,7 +41,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
- compileSdkVersion 30 // latest (or latest-1 if the sources of latest SDK are unavailable)
+ compileSdkVersion 30
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -54,24 +53,14 @@ android {
defaultConfig {
applicationId "deckers.thibault.aves"
- // some Java 8 APIs (java.util.stream, etc.) require minSdkVersion 24
- // Gradle plugin 4.0 desugaring features allow targeting older SDKs
- // but Flutter (as of v1.17.3) fails to run in release mode when using Gradle plugin 4.0:
- // https://github.com/flutter/flutter/issues/58247
+ // TODO TLAD try minSdkVersion 23 when kotlin migration is done
minSdkVersion 24
targetSdkVersion 30 // same as compileSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
- manifestPlaceholders = [googleApiKey:keystoreProperties['googleApiKey']]
+ manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']]
}
-// compileOptions {
-// // enable support for Java 8 language APIs (stream, optional, etc.)
-// coreLibraryDesugaringEnabled true
-// sourceCompatibility JavaVersion.VERSION_1_8
-// targetCompatibility JavaVersion.VERSION_1_8
-// }
-
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
@@ -105,22 +94,16 @@ repositories {
}
dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- // enable support for Java 8 language APIs (stream, optional, etc.)
-// coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
-
- implementation 'androidx.core:core:1.5.0-alpha04' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
- implementation "androidx.exifinterface:exifinterface:1.3.0"
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
+ implementation 'androidx.core:core-ktx:1.5.0-alpha04' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
+ implementation 'androidx.exifinterface:exifinterface:1.3.1'
implementation 'com.commonsware.cwac:document:0.4.1'
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
- implementation 'com.google.guava:guava:29.0-android'
+ implementation 'com.google.guava:guava:30.0-android'
- annotationProcessor 'androidx.annotation:annotation:1.1.0'
- annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+ kapt 'androidx.annotation:annotation:1.1.0'
+ kapt 'com.github.bumptech.glide:compiler:4.11.0'
compileOnly rootProject.findProject(':streams_channel')
}
-
-apply plugin: 'com.google.gms.google-services'
-apply plugin: 'com.google.firebase.crashlytics'
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
deleted file mode 100644
index ebcc4799f..000000000
--- a/android/app/src/debug/AndroidManifest.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ac8bdd2e6..0fdcda2c5 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -24,7 +24,8 @@
+ android:maxSdkVersion="29"
+ tools:ignore="ScopedStorage" />
diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java
deleted file mode 100644
index 0c586276a..000000000
--- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppAdapterHandler.java
+++ /dev/null
@@ -1,283 +0,0 @@
-package deckers.thibault.aves.channel.calls;
-
-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.pm.ResolveInfo;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.content.FileProvider;
-
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.load.DecodeFormat;
-import com.bumptech.glide.request.FutureTarget;
-import com.bumptech.glide.request.RequestOptions;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import deckers.thibault.aves.utils.LogUtils;
-import io.flutter.plugin.common.MethodCall;
-import io.flutter.plugin.common.MethodChannel;
-
-public class AppAdapterHandler implements MethodChannel.MethodCallHandler {
- private static final String LOG_TAG = LogUtils.createTag(AppAdapterHandler.class);
-
- public static final String CHANNEL = "deckers.thibault/aves/app";
-
- private Context context;
-
- public AppAdapterHandler(Context context) {
- this.context = context;
- }
-
- @Override
- public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- switch (call.method) {
- case "getAppIcon": {
- new Thread(() -> getAppIcon(call, new MethodResultWrapper(result))).start();
- break;
- }
- case "getAppNames": {
- result.success(getAppNames());
- break;
- }
- case "getEnv": {
- result.success(getEnv());
- break;
- }
- case "edit": {
- String title = call.argument("title");
- Uri uri = Uri.parse(call.argument("uri"));
- String mimeType = call.argument("mimeType");
- edit(title, uri, mimeType);
- result.success(null);
- break;
- }
- case "open": {
- String title = call.argument("title");
- Uri uri = Uri.parse(call.argument("uri"));
- String mimeType = call.argument("mimeType");
- open(title, uri, mimeType);
- result.success(null);
- break;
- }
- case "openMap": {
- Uri geoUri = Uri.parse(call.argument("geoUri"));
- openMap(geoUri);
- result.success(null);
- break;
- }
- case "setAs": {
- String title = call.argument("title");
- Uri uri = Uri.parse(call.argument("uri"));
- String mimeType = call.argument("mimeType");
- setAs(title, uri, mimeType);
- result.success(null);
- break;
- }
- case "share": {
- String title = call.argument("title");
- Map> urisByMimeType = call.argument("urisByMimeType");
- shareMultiple(title, urisByMimeType);
- result.success(null);
- break;
- }
- default:
- result.notImplemented();
- break;
- }
- }
-
- private Map getAppNames() {
- Map nameMap = new HashMap<>();
- Intent intent = new Intent(Intent.ACTION_MAIN, null);
- intent.addCategory(Intent.CATEGORY_LAUNCHER);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
-
- // apps tend to use their name in English when creating folders
- // so we get their names in English as well as the current locale
- Configuration config = new Configuration();
- config.setLocale(Locale.ENGLISH);
-
- PackageManager pm = context.getPackageManager();
- List resolveInfoList = pm.queryIntentActivities(intent, 0);
- for (ResolveInfo resolveInfo : resolveInfoList) {
- ApplicationInfo ai = resolveInfo.activityInfo.applicationInfo;
- boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
- if (!isSystemPackage) {
- String packageName = ai.packageName;
-
- String currentLabel = String.valueOf(pm.getApplicationLabel(ai));
- nameMap.put(currentLabel, packageName);
-
- int labelRes = ai.labelRes;
- if (labelRes != 0) {
- try {
- Resources resources = pm.getResourcesForApplication(ai);
- // `updateConfiguration` is deprecated but it seems to be the only way
- // to query resources from another app with a specific locale.
- // The following methods do not work:
- // - `resources.getConfiguration().setLocale(...)`
- // - getting a package manager from a custom context with `context.createConfigurationContext(config)`
- resources.updateConfiguration(config, resources.getDisplayMetrics());
- String englishLabel = resources.getString(labelRes);
- if (!TextUtils.equals(englishLabel, currentLabel)) {
- nameMap.put(englishLabel, packageName);
- }
- } catch (PackageManager.NameNotFoundException e) {
- Log.w(LOG_TAG, "failed to get app englishLabel for packageName=" + packageName, e);
- }
- }
- }
- }
- return nameMap;
- }
-
- private void getAppIcon(MethodCall call, MethodChannel.Result result) {
- String packageName = call.argument("packageName");
- Double sizeDip = call.argument("sizeDip");
- if (packageName == null || sizeDip == null) {
- result.error("getAppIcon-args", "failed because of missing arguments", null);
- return;
- }
-
- // convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter
- float density = context.getResources().getDisplayMetrics().density;
- int size = (int) Math.round(sizeDip * density);
-
- byte[] data = null;
- try {
- int iconResourceId = context.getPackageManager().getApplicationInfo(packageName, 0).icon;
- Uri uri = new Uri.Builder()
- .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
- .authority(packageName)
- .path(String.valueOf(iconResourceId))
- .build();
-
- RequestOptions options = new RequestOptions()
- .format(DecodeFormat.PREFER_RGB_565)
- .centerCrop()
- .override(size, size);
- FutureTarget target = Glide.with(context)
- .asBitmap()
- .apply(options)
- .load(uri)
- .submit(size, size);
-
- try {
- Bitmap bitmap = target.get();
- if (bitmap != null) {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream);
- data = stream.toByteArray();
- }
- } catch (Exception e) {
- Log.w(LOG_TAG, "failed to decode app icon for packageName=" + packageName, e);
- }
- Glide.with(context).clear(target);
- } catch (PackageManager.NameNotFoundException e) {
- Log.w(LOG_TAG, "failed to get app info for packageName=" + packageName, e);
- return;
- }
- if (data != null) {
- result.success(data);
- } else {
- result.error("getAppIcon-null", "failed to get icon for packageName=" + packageName, null);
- }
- }
-
- private Map getEnv() {
- return System.getenv();
- }
-
- private void edit(String title, Uri uri, String mimeType) {
- Intent intent = new Intent(Intent.ACTION_EDIT);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- intent.setDataAndType(uri, mimeType);
- context.startActivity(Intent.createChooser(intent, title));
- }
-
- private void open(String title, Uri uri, String mimeType) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setDataAndType(uri, mimeType);
- context.startActivity(Intent.createChooser(intent, title));
- }
-
- private void openMap(Uri geoUri) {
- Intent intent = new Intent(Intent.ACTION_VIEW, geoUri);
- if (intent.resolveActivity(context.getPackageManager()) != null) {
- context.startActivity(intent);
- }
- }
-
- private void setAs(String title, Uri uri, String mimeType) {
- Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
- intent.setDataAndType(uri, mimeType);
- context.startActivity(Intent.createChooser(intent, title));
- }
-
- private void shareSingle(String title, Uri uri, String mimeType) {
- Intent intent = new Intent(Intent.ACTION_SEND);
- if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) {
- String path = uri.getPath();
- if (path == null) return;
- String applicationId = context.getApplicationContext().getPackageName();
- Uri apkUri = FileProvider.getUriForFile(context, applicationId + ".fileprovider", new File(path));
- intent.putExtra(Intent.EXTRA_STREAM, apkUri);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- } else {
- intent.putExtra(Intent.EXTRA_STREAM, uri);
- }
- intent.setType(mimeType);
- context.startActivity(Intent.createChooser(intent, title));
- }
-
- private void shareMultiple(String title, @Nullable Map> urisByMimeType) {
- if (urisByMimeType == null) return;
-
- ArrayList uriList = urisByMimeType.values().stream().flatMap(Collection::stream).map(Uri::parse).collect(Collectors.toCollection(ArrayList::new));
- String[] mimeTypes = urisByMimeType.keySet().toArray(new String[0]);
-
- // simplify share intent for a single item, as some apps can handle one item but not more
- if (uriList.size() == 1) {
- shareSingle(title, uriList.get(0), mimeTypes[0]);
- return;
- }
-
- String mimeType = "*/*";
- if (mimeTypes.length == 1) {
- // items have the same mime type & subtype
- mimeType = mimeTypes[0];
- } else {
- // items have different subtypes
- String[] mimeTypeTypes = Arrays.stream(mimeTypes).map(mt -> mt.split("/")[0]).distinct().toArray(String[]::new);
- if (mimeTypeTypes.length == 1) {
- // items have the same mime type
- mimeType = mimeTypeTypes[0] + "/*";
- }
- }
-
- Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
- intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList);
- intent.setType(mimeType);
- context.startActivity(Intent.createChooser(intent, title));
- }
-}
diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppShortcutHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppShortcutHandler.java
deleted file mode 100644
index 9d6e3c64c..000000000
--- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/AppShortcutHandler.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package deckers.thibault.aves.channel.calls;
-
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.text.TextUtils;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.content.pm.ShortcutInfoCompat;
-import androidx.core.content.pm.ShortcutManagerCompat;
-import androidx.core.graphics.drawable.IconCompat;
-
-import java.util.List;
-
-import deckers.thibault.aves.MainActivity;
-import deckers.thibault.aves.R;
-import deckers.thibault.aves.utils.BitmapUtils;
-import io.flutter.plugin.common.MethodCall;
-import io.flutter.plugin.common.MethodChannel;
-
-public class AppShortcutHandler implements MethodChannel.MethodCallHandler {
- public static final String CHANNEL = "deckers.thibault/aves/shortcut";
-
- private Context context;
-
- public AppShortcutHandler(Context context) {
- this.context = context;
- }
-
- @Override
- public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- switch (call.method) {
- case "canPin": {
- result.success(ShortcutManagerCompat.isRequestPinShortcutSupported(context));
- break;
- }
- case "pin": {
- String label = call.argument("label");
- byte[] iconBytes = call.argument("iconBytes");
- List filters = call.argument("filters");
- new Thread(() -> pin(label, iconBytes, filters)).start();
- result.success(null);
- break;
- }
- default:
- result.notImplemented();
- break;
- }
- }
-
- private void pin(String label, byte[] iconBytes, @Nullable List filters) {
- if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context) || filters == null) {
- return;
- }
-
- IconCompat icon = null;
- if (iconBytes != null && iconBytes.length > 0) {
- Bitmap bitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.length);
- bitmap = BitmapUtils.centerSquareCrop(context, bitmap, 256);
- if (bitmap != null) {
- icon = IconCompat.createWithBitmap(bitmap);
- }
- }
- if (icon == null) {
- icon = IconCompat.createWithResource(context, R.mipmap.ic_shortcut_collection);
- }
-
- Intent intent = new Intent(Intent.ACTION_MAIN, null, context, MainActivity.class)
- .putExtra("page", "/collection")
- .putExtra("filters", filters.toArray(new String[0]));
-
- ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(context, "collection-" + TextUtils.join("-", filters))
- .setShortLabel(label)
- .setIcon(icon)
- .setIntent(intent)
- .build();
-
- ShortcutManagerCompat.requestPinShortcut(context, shortcut, null);
- }
-}
diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java
deleted file mode 100644
index 17c021b8d..000000000
--- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageDecodeTask.java
+++ /dev/null
@@ -1,218 +0,0 @@
-package deckers.thibault.aves.channel.calls;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.provider.MediaStore;
-import android.util.Log;
-import android.util.Size;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.load.DecodeFormat;
-import com.bumptech.glide.load.engine.DiskCacheStrategy;
-import com.bumptech.glide.request.FutureTarget;
-import com.bumptech.glide.request.RequestOptions;
-import com.bumptech.glide.signature.ObjectKey;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.concurrent.ExecutionException;
-
-import deckers.thibault.aves.decoder.VideoThumbnail;
-import deckers.thibault.aves.utils.BitmapUtils;
-import deckers.thibault.aves.utils.LogUtils;
-import deckers.thibault.aves.utils.MimeTypes;
-import io.flutter.plugin.common.MethodChannel;
-
-public class ImageDecodeTask extends AsyncTask {
- private static final String LOG_TAG = LogUtils.createTag(ImageDecodeTask.class);
-
- static class Params {
- Uri uri;
- String mimeType;
- Long dateModifiedSecs;
- Integer rotationDegrees, width, height, defaultSize;
- Boolean isFlipped;
- MethodChannel.Result result;
-
- Params(@NonNull String uri, @NonNull String mimeType, @NonNull Long dateModifiedSecs, @NonNull Integer rotationDegrees, @NonNull Boolean isFlipped, @Nullable Integer width, @Nullable Integer height, Integer defaultSize, MethodChannel.Result result) {
- this.uri = Uri.parse(uri);
- this.mimeType = mimeType;
- this.dateModifiedSecs = dateModifiedSecs;
- this.rotationDegrees = rotationDegrees;
- this.isFlipped = isFlipped;
- this.width = width;
- this.height = height;
- this.result = result;
- this.defaultSize = defaultSize;
- }
- }
-
- static class Result {
- Params params;
- byte[] data;
- String errorDetails;
-
- Result(Params params, byte[] data, String errorDetails) {
- this.params = params;
- this.data = data;
- this.errorDetails = errorDetails;
- }
- }
-
- @SuppressLint("StaticFieldLeak")
- private Activity activity;
-
- ImageDecodeTask(Activity activity) {
- this.activity = activity;
- }
-
- @Override
- protected Result doInBackground(Params... params) {
- Params p = params[0];
- Bitmap bitmap = null;
- Exception exception = null;
- if (!this.isCancelled()) {
- Integer w = p.width;
- Integer h = p.height;
- // fetch low quality thumbnails when size is not specified
- if (w == null || h == null || w == 0 || h == 0) {
- p.width = p.defaultSize;
- p.height = p.defaultSize;
- // EXIF orientations with flipping are not well supported by the Media Store:
- // the content resolver may return a thumbnail that is automatically rotated
- // according to EXIF orientation, but not flip it when necessary
- if (!p.isFlipped) {
- try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- bitmap = getThumbnailBytesByResolver(p);
- } else {
- bitmap = getThumbnailBytesByMediaStore(p);
- }
- } catch (Exception e) {
- exception = e;
- }
- }
- }
-
- // fallback if the native methods failed or for higher quality thumbnails
- try {
- if (bitmap == null) {
- bitmap = getThumbnailByGlide(p);
- }
- } catch (Exception e) {
- exception = e;
- }
- } else {
- Log.d(LOG_TAG, "getThumbnail with uri=" + p.uri + " cancelled");
- }
-
- byte[] data = null;
- if (bitmap != null) {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- // we compress the bitmap because Dart Image.memory cannot decode the raw bytes
- // Bitmap.CompressFormat.PNG is slower than JPEG
- bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
- data = stream.toByteArray();
- }
-
- String errorDetails = null;
- if (exception != null) {
- errorDetails = exception.getMessage();
- if (errorDetails != null && !errorDetails.isEmpty()) {
- errorDetails = errorDetails.split("\n", 2)[0];
- }
- }
- return new Result(p, data, errorDetails);
- }
-
- @RequiresApi(api = Build.VERSION_CODES.Q)
- private Bitmap getThumbnailBytesByResolver(Params params) throws IOException {
- ContentResolver resolver = activity.getContentResolver();
- Bitmap bitmap = resolver.loadThumbnail(params.uri, new Size(params.width, params.height), null);
- String mimeType = params.mimeType;
- if (MimeTypes.needRotationAfterContentResolverThumbnail(mimeType)) {
- bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, params.rotationDegrees, params.isFlipped);
- }
- return bitmap;
- }
-
- private Bitmap getThumbnailBytesByMediaStore(Params params) {
- long contentId = ContentUris.parseId(params.uri);
-
- ContentResolver resolver = activity.getContentResolver();
- if (MimeTypes.isVideo(params.mimeType)) {
- return MediaStore.Video.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Video.Thumbnails.MINI_KIND, null);
- } else {
- Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null);
- // from Android Q, returned thumbnail is already rotated according to EXIF orientation
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) {
- bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, params.rotationDegrees, params.isFlipped);
- }
- return bitmap;
- }
- }
-
- private Bitmap getThumbnailByGlide(Params params) throws ExecutionException, InterruptedException {
- Uri uri = params.uri;
- String mimeType = params.mimeType;
- Long dateModifiedSecs = params.dateModifiedSecs;
- Integer rotationDegrees = params.rotationDegrees;
- Boolean isFlipped = params.isFlipped;
- int width = params.width;
- int height = params.height;
-
- RequestOptions options = new RequestOptions()
- .format(DecodeFormat.PREFER_RGB_565)
- // add signature to ignore cache for images which got modified but kept the same URI
- .signature(new ObjectKey("" + dateModifiedSecs + rotationDegrees + isFlipped + width))
- .override(width, height);
-
- FutureTarget target;
- if (MimeTypes.isVideo(mimeType)) {
- options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE);
- target = Glide.with(activity)
- .asBitmap()
- .apply(options)
- .load(new VideoThumbnail(activity, uri))
- .submit(width, height);
- } else {
- target = Glide.with(activity)
- .asBitmap()
- .apply(options)
- .load(uri)
- .submit(width, height);
- }
-
- try {
- Bitmap bitmap = target.get();
- if (MimeTypes.needRotationAfterGlide(mimeType)) {
- bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped);
- }
- return bitmap;
- } finally {
- Glide.with(activity).clear(target);
- }
- }
-
- @Override
- protected void onPostExecute(Result result) {
- Params params = result.params;
- MethodChannel.Result r = params.result;
- String uri = params.uri.toString();
- if (result.data != null) {
- r.success(result.data);
- } else {
- r.error("getThumbnail-null", "failed to get thumbnail for uri=" + uri, result.errorDetails);
- }
- }
-}
diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java
deleted file mode 100644
index 79887396c..000000000
--- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java
+++ /dev/null
@@ -1,217 +0,0 @@
-package deckers.thibault.aves.channel.calls;
-
-import android.app.Activity;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
-
-import androidx.annotation.NonNull;
-
-import com.bumptech.glide.Glide;
-
-import java.util.List;
-import java.util.Map;
-
-import deckers.thibault.aves.model.ExifOrientationOp;
-import deckers.thibault.aves.model.provider.ImageProvider;
-import deckers.thibault.aves.model.provider.ImageProviderFactory;
-import deckers.thibault.aves.model.provider.MediaStoreImageProvider;
-import io.flutter.plugin.common.MethodCall;
-import io.flutter.plugin.common.MethodChannel;
-
-public class ImageFileHandler implements MethodChannel.MethodCallHandler {
- public static final String CHANNEL = "deckers.thibault/aves/image";
-
- private Activity activity;
- private float density;
-
- public ImageFileHandler(Activity activity) {
- this.activity = activity;
- }
-
- public float getDensity() {
- if (density == 0) {
- density = activity.getResources().getDisplayMetrics().density;
- }
- return density;
- }
-
- @Override
- public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- switch (call.method) {
- case "getObsoleteEntries":
- new Thread(() -> getObsoleteEntries(call, new MethodResultWrapper(result))).start();
- break;
- case "getImageEntry":
- new Thread(() -> getImageEntry(call, new MethodResultWrapper(result))).start();
- break;
- case "getThumbnail":
- new Thread(() -> getThumbnail(call, new MethodResultWrapper(result))).start();
- break;
- case "clearSizedThumbnailDiskCache":
- new Thread(() -> Glide.get(activity).clearDiskCache()).start();
- result.success(null);
- break;
- case "rename":
- new Thread(() -> rename(call, new MethodResultWrapper(result))).start();
- break;
- case "rotate":
- new Thread(() -> rotate(call, new MethodResultWrapper(result))).start();
- break;
- case "flip":
- new Thread(() -> flip(call, new MethodResultWrapper(result))).start();
- break;
- default:
- result.notImplemented();
- break;
- }
- }
-
- private void getThumbnail(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- String uri = call.argument("uri");
- String mimeType = call.argument("mimeType");
- Number dateModifiedSecs = (Number)call.argument("dateModifiedSecs");
- Integer rotationDegrees = call.argument("rotationDegrees");
- Boolean isFlipped = call.argument("isFlipped");
- Double widthDip = call.argument("widthDip");
- Double heightDip = call.argument("heightDip");
- Double defaultSizeDip = call.argument("defaultSizeDip");
-
- if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) {
- result.error("getThumbnail-args", "failed because of missing arguments", null);
- return;
- }
- // convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter
- float density = getDensity();
- int width = (int) Math.round(widthDip * density);
- int height = (int) Math.round(heightDip * density);
- int defaultSize = (int) Math.round(defaultSizeDip * density);
-
- new ImageDecodeTask(activity).execute(new ImageDecodeTask.Params(uri, mimeType, dateModifiedSecs.longValue(), rotationDegrees, isFlipped, width, height, defaultSize, result));
- }
-
- private void getObsoleteEntries(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- List known = call.argument("knownContentIds");
- if (known == null) {
- result.error("getObsoleteEntries-args", "failed because of missing arguments", null);
- return;
- }
- List obsolete = new MediaStoreImageProvider().getObsoleteContentIds(activity, known);
- result.success(obsolete);
- }
-
- private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- String mimeType = call.argument("mimeType");
- Uri uri = Uri.parse(call.argument("uri"));
-
- if (uri == null || mimeType == null) {
- result.error("getImageEntry-args", "failed because of missing arguments", null);
- return;
- }
-
- ImageProvider provider = ImageProviderFactory.getProvider(uri);
- if (provider == null) {
- result.error("getImageEntry-provider", "failed to find provider for uri=" + uri, null);
- return;
- }
-
- provider.fetchSingle(activity, uri, mimeType, new ImageProvider.ImageOpCallback() {
- @Override
- public void onSuccess(Map entry) {
- result.success(entry);
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- result.error("getImageEntry-failure", "failed to get entry for uri=" + uri, throwable.getMessage());
- }
- });
- }
-
- private void rename(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- Map entryMap = call.argument("entry");
- String newName = call.argument("newName");
- if (entryMap == null || newName == null) {
- result.error("rename-args", "failed because of missing arguments", null);
- return;
- }
- Uri uri = Uri.parse((String) entryMap.get("uri"));
- String path = (String) entryMap.get("path");
- String mimeType = (String) entryMap.get("mimeType");
-
- ImageProvider provider = ImageProviderFactory.getProvider(uri);
- if (provider == null) {
- result.error("rename-provider", "failed to find provider for uri=" + uri, null);
- return;
- }
- provider.rename(activity, path, uri, mimeType, newName, new ImageProvider.ImageOpCallback() {
- @Override
- public void onSuccess(Map newFields) {
- new Handler(Looper.getMainLooper()).post(() -> result.success(newFields));
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- new Handler(Looper.getMainLooper()).post(() -> result.error("rename-failure", "failed to rename", throwable.getMessage()));
- }
- });
- }
-
- private void rotate(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- Map entryMap = call.argument("entry");
- Boolean clockwise = call.argument("clockwise");
- if (entryMap == null || clockwise == null) {
- result.error("rotate-args", "failed because of missing arguments", null);
- return;
- }
- Uri uri = Uri.parse((String) entryMap.get("uri"));
- String path = (String) entryMap.get("path");
- String mimeType = (String) entryMap.get("mimeType");
-
- ImageProvider provider = ImageProviderFactory.getProvider(uri);
- if (provider == null) {
- result.error("rotate-provider", "failed to find provider for uri=" + uri, null);
- return;
- }
- ExifOrientationOp op = clockwise ? ExifOrientationOp.ROTATE_CW : ExifOrientationOp.ROTATE_CCW;
- provider.changeOrientation(activity, path, uri, mimeType, op, new ImageProvider.ImageOpCallback() {
- @Override
- public void onSuccess(Map newFields) {
- new Handler(Looper.getMainLooper()).post(() -> result.success(newFields));
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- new Handler(Looper.getMainLooper()).post(() -> result.error("rotate-failure", "failed to rotate", throwable.getMessage()));
- }
- });
- }
-
- private void flip(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- Map entryMap = call.argument("entry");
- if (entryMap == null) {
- result.error("flip-args", "failed because of missing arguments", null);
- return;
- }
- Uri uri = Uri.parse((String) entryMap.get("uri"));
- String path = (String) entryMap.get("path");
- String mimeType = (String) entryMap.get("mimeType");
-
- ImageProvider provider = ImageProviderFactory.getProvider(uri);
- if (provider == null) {
- result.error("flip-provider", "failed to find provider for uri=" + uri, null);
- return;
- }
- provider.changeOrientation(activity, path, uri, mimeType, ExifOrientationOp.FLIP, new ImageProvider.ImageOpCallback() {
- @Override
- public void onSuccess(Map newFields) {
- new Handler(Looper.getMainLooper()).post(() -> result.success(newFields));
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- new Handler(Looper.getMainLooper()).post(() -> result.error("flip-failure", "failed to flip", throwable.getMessage()));
- }
- });
- }
-}
\ No newline at end of file
diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/MethodResultWrapper.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/MethodResultWrapper.java
deleted file mode 100644
index e6aeaacef..000000000
--- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/MethodResultWrapper.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package deckers.thibault.aves.channel.calls;
-
-import android.os.Handler;
-import android.os.Looper;
-
-import io.flutter.plugin.common.MethodChannel;
-
-// ensure `result` methods are called on the main looper thread
-public class MethodResultWrapper implements MethodChannel.Result {
- private MethodChannel.Result methodResult;
- private Handler handler;
-
- MethodResultWrapper(MethodChannel.Result result) {
- methodResult = result;
- handler = new Handler(Looper.getMainLooper());
- }
-
- @Override
- public void success(final Object result) {
- handler.post(() -> methodResult.success(result));
- }
-
- @Override
- public void error(final String errorCode, final String errorMessage, final Object errorDetails) {
- handler.post(() -> methodResult.error(errorCode, errorMessage, errorDetails));
- }
-
- @Override
- public void notImplemented() {
- handler.post(() -> methodResult.notImplemented());
- }
-}
\ No newline at end of file
diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/StorageHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/StorageHandler.java
deleted file mode 100644
index c32c82ab4..000000000
--- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/StorageHandler.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package deckers.thibault.aves.channel.calls;
-
-import android.content.Context;
-import android.media.MediaScannerConnection;
-import android.os.Build;
-import android.os.storage.StorageManager;
-import android.os.storage.StorageVolume;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import deckers.thibault.aves.utils.PermissionManager;
-import deckers.thibault.aves.utils.StorageUtils;
-import io.flutter.plugin.common.MethodCall;
-import io.flutter.plugin.common.MethodChannel;
-
-public class StorageHandler implements MethodChannel.MethodCallHandler {
- public static final String CHANNEL = "deckers.thibault/aves/storage";
-
- private Context context;
-
- public StorageHandler(Context context) {
- this.context = context;
- }
-
- @Override
- public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- switch (call.method) {
- case "getStorageVolumes": {
- List