Kotlin migration (WIP)

This commit is contained in:
Thibault Deckers 2020-10-20 23:50:43 +09:00
parent 69d700674c
commit ed494d977e
9 changed files with 116 additions and 205 deletions

View file

@ -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')
}

View file

@ -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":

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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<InputStream> {
private final VideoThumbnail model;
VideoThumbnailFetcher(VideoThumbnail model) {
this.model = model;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> 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<InputStream> getDataClass() {
return InputStream.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.LOCAL;
}
}

View file

@ -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());
}
}

View file

@ -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<VideoThumbnail, InputStream> {
@Nullable
@Override
public LoadData<InputStream> 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<VideoThumbnail, InputStream> {
@NonNull
@Override
public ModelLoader<VideoThumbnail, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new VideoThumbnailLoader();
}
@Override
public void teardown() {
}
}
}

View file

@ -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
}

View file

@ -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<VideoThumbnail, InputStream> {
override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model))
}
override fun handles(videoThumbnail: VideoThumbnail): Boolean = true
internal class Factory : ModelLoaderFactory<VideoThumbnail, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<VideoThumbnail, InputStream> = VideoThumbnailLoader()
override fun teardown() {}
}
}
internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFetcher<InputStream> {
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
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> = InputStream::class.java
override fun getDataSource(): DataSource = DataSource.LOCAL
}