diff --git a/android/app/build.gradle b/android/app/build.gradle index d14dcdbda..fb171d5cd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,6 +23,7 @@ if (flutterVersionName == null) { apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" def keystoreProperties = new Properties() @@ -54,10 +55,7 @@ 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() @@ -65,13 +63,6 @@ android { 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'] @@ -106,18 +97,15 @@ 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.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: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') } 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 index c32c82ab4..3b85e8fa2 100644 --- 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 @@ -23,7 +23,7 @@ import io.flutter.plugin.common.MethodChannel; public class StorageHandler implements MethodChannel.MethodCallHandler { public static final String CHANNEL = "deckers.thibault/aves/storage"; - private Context context; + private final Context context; public StorageHandler(Context context) { this.context = context; @@ -54,7 +54,9 @@ public class StorageHandler implements MethodChannel.MethodCallHandler { } case "revokeDirectoryAccess": String path = call.argument("path"); - PermissionManager.revokeDirectoryAccess(context, path); + if (path != null) { + PermissionManager.revokeDirectoryAccess(context, path); + } result.success(true); break; case "getGrantedDirectories": diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/AvesAppGlideModule.java b/android/app/src/main/java/deckers/thibault/aves/decoder/AvesAppGlideModule.java deleted file mode 100644 index 55dac0d89..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/AvesAppGlideModule.java +++ /dev/null @@ -1,34 +0,0 @@ -package deckers.thibault.aves.decoder; - -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.GlideBuilder; -import com.bumptech.glide.Registry; -import com.bumptech.glide.annotation.GlideModule; -import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser; -import com.bumptech.glide.module.AppGlideModule; - -@GlideModule -public class AvesAppGlideModule extends AppGlideModule { - @Override - public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { - // hide noisy warning (e.g. for images that can't be decoded) - builder.setLogLevel(Log.ERROR); - } - - @Override - public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { - // prevent ExifInterface error logs - // cf https://github.com/bumptech/glide/issues/3383 - glide.getRegistry().getImageHeaderParsers().removeIf(parser -> parser instanceof ExifInterfaceImageHeaderParser); - } - - @Override - public boolean isManifestParsingEnabled() { - return false; - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnail.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnail.java deleted file mode 100644 index ff76284be..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnail.java +++ /dev/null @@ -1,22 +0,0 @@ -package deckers.thibault.aves.decoder; - -import android.content.Context; -import android.net.Uri; - -public class VideoThumbnail { - private Context mContext; - private Uri mUri; - - public VideoThumbnail(Context context, Uri uri) { - mContext = context; - mUri = uri; - } - - public Context getContext() { - return mContext; - } - - Uri getUri() { - return mUri; - } -} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailFetcher.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailFetcher.java deleted file mode 100644 index 9c357eb6f..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailFetcher.java +++ /dev/null @@ -1,73 +0,0 @@ -package deckers.thibault.aves.decoder; - -import android.graphics.Bitmap; -import android.media.MediaMetadataRetriever; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.Priority; -import com.bumptech.glide.load.DataSource; -import com.bumptech.glide.load.data.DataFetcher; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; - -import deckers.thibault.aves.utils.StorageUtils; - -class VideoThumbnailFetcher implements DataFetcher { - private final VideoThumbnail model; - - VideoThumbnailFetcher(VideoThumbnail model) { - this.model = model; - } - - @Override - public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { - MediaMetadataRetriever retriever = StorageUtils.openMetadataRetriever(model.getContext(), model.getUri()); - if (retriever != null) { - try { - byte[] picture = retriever.getEmbeddedPicture(); - if (picture != null) { - callback.onDataReady(new ByteArrayInputStream(picture)); - } else { - // not ideal: bitmap -> byte[] -> bitmap - // but simple fallback and we cache result - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Bitmap bitmap = retriever.getFrameAtTime(); - if (bitmap != null) { - bitmap.compress(Bitmap.CompressFormat.PNG, 0, bos); - } - callback.onDataReady(new ByteArrayInputStream(bos.toByteArray())); - } - } catch (Exception e) { - callback.onLoadFailed(e); - } finally { - // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs - retriever.release(); - } - } - } - - @Override - public void cleanup() { - // already cleaned up in loadData and ByteArrayInputStream will be GC'd - } - - @Override - public void cancel() { - // cannot cancel - } - - @NonNull - @Override - public Class getDataClass() { - return InputStream.class; - } - - @NonNull - @Override - public DataSource getDataSource() { - return DataSource.LOCAL; - } -} \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.java deleted file mode 100644 index 2912c1ebc..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.java +++ /dev/null @@ -1,20 +0,0 @@ -package deckers.thibault.aves.decoder; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.Registry; -import com.bumptech.glide.annotation.GlideModule; -import com.bumptech.glide.module.LibraryGlideModule; - -import java.io.InputStream; - -@GlideModule -public class VideoThumbnailGlideModule extends LibraryGlideModule { - @Override - public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { - registry.append(VideoThumbnail.class, InputStream.class, new VideoThumbnailLoader.Factory()); - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailLoader.java b/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailLoader.java deleted file mode 100644 index 6b818eefa..000000000 --- a/android/app/src/main/java/deckers/thibault/aves/decoder/VideoThumbnailLoader.java +++ /dev/null @@ -1,37 +0,0 @@ -package deckers.thibault.aves.decoder; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.bumptech.glide.load.Options; -import com.bumptech.glide.load.model.ModelLoader; -import com.bumptech.glide.load.model.ModelLoaderFactory; -import com.bumptech.glide.load.model.MultiModelLoaderFactory; -import com.bumptech.glide.signature.ObjectKey; - -import java.io.InputStream; - -class VideoThumbnailLoader implements ModelLoader { - @Nullable - @Override - public LoadData buildLoadData(@NonNull VideoThumbnail model, int width, int height, @NonNull Options options) { - return new LoadData<>(new ObjectKey(model.getUri()), new VideoThumbnailFetcher(model)); - } - - @Override - public boolean handles(@NonNull VideoThumbnail videoThumbnail) { - return true; - } - - static class Factory implements ModelLoaderFactory { - @NonNull - @Override - public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { - return new VideoThumbnailLoader(); - } - - @Override - public void teardown() { - } - } -} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt new file mode 100644 index 000000000..59d9725cb --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt @@ -0,0 +1,27 @@ +package deckers.thibault.aves.decoder + +import android.content.Context +import android.util.Log +import com.bumptech.glide.Glide +import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.ImageHeaderParser +import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser +import com.bumptech.glide.module.AppGlideModule + +@GlideModule +class AvesAppGlideModule : AppGlideModule() { + override fun applyOptions(context: Context, builder: GlideBuilder) { + // hide noisy warning (e.g. for images that can't be decoded) + builder.setLogLevel(Log.ERROR) + } + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + // prevent ExifInterface error logs + // cf https://github.com/bumptech/glide/issues/3383 + glide.registry.imageHeaderParsers.removeIf { parser: ImageHeaderParser? -> parser is ExifInterfaceImageHeaderParser } + } + + override fun isManifestParsingEnabled(): Boolean = false +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt new file mode 100644 index 000000000..6db0c5c02 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt @@ -0,0 +1,80 @@ +package deckers.thibault.aves.decoder + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import com.bumptech.glide.Glide +import com.bumptech.glide.Priority +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.data.DataFetcher.DataCallback +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.module.LibraryGlideModule +import com.bumptech.glide.signature.ObjectKey +import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream + +@GlideModule +class VideoThumbnailGlideModule : LibraryGlideModule() { + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + registry.append(VideoThumbnail::class.java, InputStream::class.java, VideoThumbnailLoader.Factory()) + } +} + +class VideoThumbnail(val context: Context, val uri: Uri) + +internal class VideoThumbnailLoader : ModelLoader { + override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData? { + return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model)) + } + + override fun handles(videoThumbnail: VideoThumbnail): Boolean = true + + internal class Factory : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader = VideoThumbnailLoader() + + override fun teardown() {} + } +} + +internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFetcher { + override fun loadData(priority: Priority, callback: DataCallback) { + val retriever = openMetadataRetriever(model.context, model.uri) + if (retriever != null) { + try { + var picture = retriever.embeddedPicture + if (picture == null) { + // not ideal: bitmap -> byte[] -> bitmap + // but simple fallback and we cache result + val bos = ByteArrayOutputStream() + val bitmap = retriever.frameAtTime + bitmap?.compress(Bitmap.CompressFormat.PNG, 0, bos) + picture = bos.toByteArray() + } + callback.onDataReady(ByteArrayInputStream(picture)) + } catch (e: Exception) { + callback.onLoadFailed(e) + } finally { + // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs + retriever.release() + } + } + } + + // already cleaned up in loadData and ByteArrayInputStream will be GC'd + override fun cleanup() {} + + // cannot cancel + override fun cancel() {} + + override fun getDataClass(): Class = InputStream::class.java + + override fun getDataSource(): DataSource = DataSource.LOCAL +} \ No newline at end of file