Kotlin migration (WIP)
This commit is contained in:
parent
87dc1768dd
commit
179fe36b8d
10 changed files with 277 additions and 288 deletions
|
@ -1,107 +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 final 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<Map<String, Object>> volumes;
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
|
||||||
volumes = getStorageVolumes();
|
|
||||||
} else {
|
|
||||||
// TODO TLAD find alternative for Android <N
|
|
||||||
volumes = new ArrayList<>();
|
|
||||||
}
|
|
||||||
result.success(volumes);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "getInaccessibleDirectories": {
|
|
||||||
List<String> dirPaths = call.argument("dirPaths");
|
|
||||||
if (dirPaths == null) {
|
|
||||||
result.error("getInaccessibleDirectories-args", "failed because of missing arguments", null);
|
|
||||||
} else {
|
|
||||||
result.success(PermissionManager.getInaccessibleDirectories(context, dirPaths));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "revokeDirectoryAccess":
|
|
||||||
String path = call.argument("path");
|
|
||||||
if (path != null) {
|
|
||||||
PermissionManager.revokeDirectoryAccess(context, path);
|
|
||||||
}
|
|
||||||
result.success(true);
|
|
||||||
break;
|
|
||||||
case "getGrantedDirectories":
|
|
||||||
result.success(new ArrayList<>(PermissionManager.getGrantedDirs(context)));
|
|
||||||
break;
|
|
||||||
case "scanFile":
|
|
||||||
scanFile(call, new MethodResultWrapper(result));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
result.notImplemented();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
private List<Map<String, Object>> getStorageVolumes() {
|
|
||||||
List<Map<String, Object>> volumes = new ArrayList<>();
|
|
||||||
StorageManager sm = context.getSystemService(StorageManager.class);
|
|
||||||
if (sm != null) {
|
|
||||||
for (String volumePath : StorageUtils.getVolumePaths(context)) {
|
|
||||||
try {
|
|
||||||
StorageVolume volume = sm.getStorageVolume(new File(volumePath));
|
|
||||||
if (volume != null) {
|
|
||||||
Map<String, Object> volumeMap = new HashMap<>();
|
|
||||||
volumeMap.put("path", volumePath);
|
|
||||||
volumeMap.put("description", volume.getDescription(context));
|
|
||||||
volumeMap.put("isPrimary", volume.isPrimary());
|
|
||||||
volumeMap.put("isRemovable", volume.isRemovable());
|
|
||||||
volumeMap.put("isEmulated", volume.isEmulated());
|
|
||||||
volumeMap.put("state", volume.getState());
|
|
||||||
volumes.add(volumeMap);
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return volumes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scanFile(MethodCall call, MethodChannel.Result result) {
|
|
||||||
String path = call.argument("path");
|
|
||||||
String mimeType = call.argument("mimeType");
|
|
||||||
MediaScannerConnection.scanFile(context, new String[]{path}, new String[]{mimeType}, (p, uri) -> {
|
|
||||||
result.success(uri != null ? uri.toString() : null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
package deckers.thibault.aves.channel.streams;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
|
|
||||||
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 java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import deckers.thibault.aves.decoder.VideoThumbnail;
|
|
||||||
import deckers.thibault.aves.utils.BitmapUtils;
|
|
||||||
import deckers.thibault.aves.utils.MimeTypes;
|
|
||||||
import deckers.thibault.aves.utils.StorageUtils;
|
|
||||||
import io.flutter.plugin.common.EventChannel;
|
|
||||||
|
|
||||||
public class ImageByteStreamHandler implements EventChannel.StreamHandler {
|
|
||||||
public static final String CHANNEL = "deckers.thibault/aves/imagebytestream";
|
|
||||||
|
|
||||||
private Activity activity;
|
|
||||||
private Uri uri;
|
|
||||||
private String mimeType;
|
|
||||||
private int rotationDegrees;
|
|
||||||
private boolean isFlipped;
|
|
||||||
private EventChannel.EventSink eventSink;
|
|
||||||
private Handler handler;
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public ImageByteStreamHandler(Activity activity, Object arguments) {
|
|
||||||
this.activity = activity;
|
|
||||||
if (arguments instanceof Map) {
|
|
||||||
Map<String, Object> argMap = (Map<String, Object>) arguments;
|
|
||||||
this.mimeType = (String) argMap.get("mimeType");
|
|
||||||
this.uri = Uri.parse((String) argMap.get("uri"));
|
|
||||||
this.rotationDegrees = (int) argMap.get("rotationDegrees");
|
|
||||||
this.isFlipped = (boolean) argMap.get("isFlipped");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onListen(Object args, EventChannel.EventSink eventSink) {
|
|
||||||
this.eventSink = eventSink;
|
|
||||||
this.handler = new Handler(Looper.getMainLooper());
|
|
||||||
new Thread(this::getImage).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCancel(Object o) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private void success(final byte[] bytes) {
|
|
||||||
handler.post(() -> eventSink.success(bytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void error(final String errorCode, final String errorMessage, final Object errorDetails) {
|
|
||||||
handler.post(() -> eventSink.error(errorCode, errorMessage, errorDetails));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void endOfStream() {
|
|
||||||
handler.post(() -> eventSink.endOfStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supported image formats:
|
|
||||||
// - Flutter (as of v1.20): JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP
|
|
||||||
// - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats
|
|
||||||
// - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java
|
|
||||||
private void getImage() {
|
|
||||||
// request a fresh image with the highest quality format
|
|
||||||
RequestOptions options = new RequestOptions()
|
|
||||||
.format(DecodeFormat.PREFER_ARGB_8888)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.skipMemoryCache(true);
|
|
||||||
|
|
||||||
if (MimeTypes.isVideo(mimeType)) {
|
|
||||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
|
||||||
.asBitmap()
|
|
||||||
.apply(options)
|
|
||||||
.load(new VideoThumbnail(activity, uri))
|
|
||||||
.submit();
|
|
||||||
try {
|
|
||||||
Bitmap bitmap = target.get();
|
|
||||||
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);
|
|
||||||
success(stream.toByteArray());
|
|
||||||
} else {
|
|
||||||
error("getImage-video-null", "failed to get image from uri=" + uri, null);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
error("getImage-video-exception", "failed to get image from uri=" + uri, e.getMessage());
|
|
||||||
} finally {
|
|
||||||
Glide.with(activity).clear(target);
|
|
||||||
}
|
|
||||||
} else if (!MimeTypes.isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
|
|
||||||
// we convert the image on platform side first, when Dart Image.memory does not support it
|
|
||||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
|
||||||
.asBitmap()
|
|
||||||
.apply(options)
|
|
||||||
.load(uri)
|
|
||||||
.submit();
|
|
||||||
try {
|
|
||||||
Bitmap bitmap = target.get();
|
|
||||||
if (MimeTypes.needRotationAfterGlide(mimeType)) {
|
|
||||||
bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped);
|
|
||||||
}
|
|
||||||
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, but it allows transparency
|
|
||||||
if (MimeTypes.canHaveAlpha(mimeType)) {
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
|
||||||
} else {
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
|
|
||||||
}
|
|
||||||
success(stream.toByteArray());
|
|
||||||
} else {
|
|
||||||
error("getImage-image-decode-null", "failed to get image from uri=" + uri, null);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
String errorDetails = e.getMessage();
|
|
||||||
if (errorDetails != null && !errorDetails.isEmpty()) {
|
|
||||||
errorDetails = errorDetails.split("\n", 2)[0];
|
|
||||||
}
|
|
||||||
error("getImage-image-decode-exception", "failed to get image from uri=" + uri, errorDetails);
|
|
||||||
} finally {
|
|
||||||
Glide.with(activity).clear(target);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try (InputStream is = StorageUtils.openInputStream(activity, uri)) {
|
|
||||||
if (is != null) {
|
|
||||||
streamBytes(is);
|
|
||||||
} else {
|
|
||||||
error("getImage-image-read-null", "failed to get image from uri=" + uri, null);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
error("getImage-image-read-exception", "failed to get image from uri=" + uri, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endOfStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void streamBytes(InputStream inputStream) throws IOException {
|
|
||||||
int bufferSize = 2 << 17; // 256kB
|
|
||||||
byte[] buffer = new byte[bufferSize];
|
|
||||||
int len;
|
|
||||||
while ((len = inputStream.read(buffer)) != -1) {
|
|
||||||
// cannot decode image on Flutter side when using `buffer` directly...
|
|
||||||
byte[] sub = new byte[len];
|
|
||||||
System.arraycopy(buffer, 0, sub, 0, len);
|
|
||||||
success(sub);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,7 +27,7 @@ public class ImageOpStreamHandler implements EventChannel.StreamHandler {
|
||||||
|
|
||||||
public static final String CHANNEL = "deckers.thibault/aves/imageopstream";
|
public static final String CHANNEL = "deckers.thibault/aves/imageopstream";
|
||||||
|
|
||||||
private Context context;
|
private final Context context;
|
||||||
private EventChannel.EventSink eventSink;
|
private EventChannel.EventSink eventSink;
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
private Map<String, Object> argMap;
|
private Map<String, Object> argMap;
|
||||||
|
|
|
@ -141,9 +141,9 @@ class MainActivity : FlutterActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) {
|
if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) {
|
||||||
val treeUri = data.data
|
val treeUri = data?.data
|
||||||
if (resultCode != RESULT_OK || treeUri == null) {
|
if (resultCode != RESULT_OK || treeUri == null) {
|
||||||
PermissionManager.onPermissionResult(requestCode, null)
|
PermissionManager.onPermissionResult(requestCode, null)
|
||||||
return
|
return
|
||||||
|
|
|
@ -212,19 +212,19 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
|
|
||||||
// simplify share intent for a single item, as some apps can handle one item but not more
|
// simplify share intent for a single item, as some apps can handle one item but not more
|
||||||
if (uriList.size == 1) {
|
if (uriList.size == 1) {
|
||||||
return shareSingle(title, uriList[0], mimeTypes[0])
|
return shareSingle(title, uriList.first(), mimeTypes.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
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[0]
|
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[0].toString() + "/*"
|
mimeType = mimeTypeTypes.first().toString() + "/*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
package deckers.thibault.aves.channel.calls
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.storage.StorageManager
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import deckers.thibault.aves.utils.PermissionManager
|
||||||
|
import deckers.thibault.aves.utils.StorageUtils.getVolumePaths
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class StorageHandler(private val context: Context) : MethodCallHandler {
|
||||||
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
when (call.method) {
|
||||||
|
"getStorageVolumes" -> {
|
||||||
|
val volumes: List<Map<String, Any>> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
storageVolumes
|
||||||
|
} else {
|
||||||
|
// TODO TLAD find alternative for Android <N
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
result.success(volumes)
|
||||||
|
}
|
||||||
|
"getGrantedDirectories" -> result.success(ArrayList(PermissionManager.getGrantedDirs(context)))
|
||||||
|
"getInaccessibleDirectories" -> getInaccessibleDirectories(call, result)
|
||||||
|
"revokeDirectoryAccess" -> revokeDirectoryAccess(call, result)
|
||||||
|
"scanFile" -> scanFile(call, MethodResultWrapper(result))
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val storageVolumes: List<Map<String, Any>>
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||||
|
get() {
|
||||||
|
val volumes = ArrayList<Map<String, Any>>()
|
||||||
|
val sm = context.getSystemService(StorageManager::class.java)
|
||||||
|
if (sm != null) {
|
||||||
|
for (volumePath in getVolumePaths(context)) {
|
||||||
|
try {
|
||||||
|
sm.getStorageVolume(File(volumePath))?.let {
|
||||||
|
val volumeMap: MutableMap<String, Any> = HashMap()
|
||||||
|
volumeMap["path"] = volumePath
|
||||||
|
volumeMap["description"] = it.getDescription(context)
|
||||||
|
volumeMap["isPrimary"] = it.isPrimary
|
||||||
|
volumeMap["isRemovable"] = it.isRemovable
|
||||||
|
volumeMap["isEmulated"] = it.isEmulated
|
||||||
|
volumeMap["state"] = it.state
|
||||||
|
volumes.add(volumeMap)
|
||||||
|
}
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return volumes
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val dirPaths = call.argument<List<String>>("dirPaths")
|
||||||
|
if (dirPaths == null) {
|
||||||
|
result.error("getInaccessibleDirectories-args", "failed because of missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val dirs = PermissionManager.getInaccessibleDirectories(context, dirPaths)
|
||||||
|
result.success(dirs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun revokeDirectoryAccess(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val path = call.argument<String>("path")
|
||||||
|
if (path == null) {
|
||||||
|
result.error("revokeDirectoryAccess-args", "failed because of missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val success = PermissionManager.revokeDirectoryAccess(context, path)
|
||||||
|
result.success(success)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scanFile(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val path = call.argument<String>("path")
|
||||||
|
val mimeType = call.argument<String>("mimeType")
|
||||||
|
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, uri: Uri? -> result.success(uri?.toString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CHANNEL = "deckers.thibault/aves/storage"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
package deckers.thibault.aves.channel.streams
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import deckers.thibault.aves.decoder.VideoThumbnail
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes.canHaveAlpha
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
||||||
|
import deckers.thibault.aves.utils.StorageUtils.openInputStream
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class ImageByteStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
|
||||||
|
private lateinit var eventSink: EventSink
|
||||||
|
private lateinit var handler: Handler
|
||||||
|
|
||||||
|
override fun onListen(args: Any, eventSink: EventSink) {
|
||||||
|
this.eventSink = eventSink
|
||||||
|
handler = Handler(Looper.getMainLooper())
|
||||||
|
Thread { streamImage() }.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancel(o: Any) {}
|
||||||
|
|
||||||
|
private fun success(bytes: ByteArray) {
|
||||||
|
handler.post { eventSink.success(bytes) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun error(errorCode: String, errorMessage: String, errorDetails: Any?) {
|
||||||
|
handler.post { eventSink.error(errorCode, errorMessage, errorDetails) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun endOfStream() {
|
||||||
|
handler.post { eventSink.endOfStream() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported image formats:
|
||||||
|
// - Flutter (as of v1.20): JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP
|
||||||
|
// - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats
|
||||||
|
// - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java
|
||||||
|
private fun streamImage() {
|
||||||
|
if (arguments !is Map<*, *>) {
|
||||||
|
endOfStream()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val mimeType = arguments["mimeType"] as String?
|
||||||
|
val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) }
|
||||||
|
val rotationDegrees = arguments["rotationDegrees"] as Int
|
||||||
|
val isFlipped = arguments["isFlipped"] as Boolean
|
||||||
|
|
||||||
|
if (mimeType == null || uri == null) {
|
||||||
|
error("streamImage-args", "failed because of missing arguments", null)
|
||||||
|
endOfStream()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVideo(mimeType)) {
|
||||||
|
streamVideoByGlide(uri)
|
||||||
|
} else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
|
||||||
|
// decode exotic format on platform side, then encode it in portable format for Flutter
|
||||||
|
streamImageByGlide(uri, mimeType, rotationDegrees, isFlipped)
|
||||||
|
} else {
|
||||||
|
// to be decoded by Flutter
|
||||||
|
streamImageAsIs(uri)
|
||||||
|
}
|
||||||
|
endOfStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun streamImageAsIs(uri: Uri) {
|
||||||
|
try {
|
||||||
|
openInputStream(activity, uri).use { input -> input?.let { streamBytes(it) } }
|
||||||
|
} catch (e: IOException) {
|
||||||
|
error("streamImage-image-read-exception", "failed to get image from uri=$uri", e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun streamImageByGlide(uri: Uri, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) {
|
||||||
|
val target = Glide.with(activity)
|
||||||
|
.asBitmap()
|
||||||
|
.apply(options)
|
||||||
|
.load(uri)
|
||||||
|
.submit()
|
||||||
|
try {
|
||||||
|
var bitmap = target.get()
|
||||||
|
if (needRotationAfterGlide(mimeType)) {
|
||||||
|
bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped)
|
||||||
|
}
|
||||||
|
if (bitmap != null) {
|
||||||
|
val stream = ByteArrayOutputStream()
|
||||||
|
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
||||||
|
// Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency
|
||||||
|
if (canHaveAlpha(mimeType)) {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
||||||
|
} else {
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
|
||||||
|
}
|
||||||
|
success(stream.toByteArray())
|
||||||
|
} else {
|
||||||
|
error("streamImage-image-decode-null", "failed to get image from uri=$uri", null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
var errorDetails = e.message
|
||||||
|
if (errorDetails?.isNotEmpty() == true) {
|
||||||
|
errorDetails = errorDetails.split("\n".toRegex(), 2).first()
|
||||||
|
}
|
||||||
|
error("streamImage-image-decode-exception", "failed to get image from uri=$uri", errorDetails)
|
||||||
|
} finally {
|
||||||
|
Glide.with(activity).clear(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun streamVideoByGlide(uri: Uri) {
|
||||||
|
val target = Glide.with(activity)
|
||||||
|
.asBitmap()
|
||||||
|
.apply(options)
|
||||||
|
.load(VideoThumbnail(activity, uri))
|
||||||
|
.submit()
|
||||||
|
try {
|
||||||
|
val bitmap = target.get()
|
||||||
|
if (bitmap != null) {
|
||||||
|
val stream = 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, 100, stream)
|
||||||
|
success(stream.toByteArray())
|
||||||
|
} else {
|
||||||
|
error("streamImage-video-null", "failed to get image from uri=$uri", null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error("streamImage-video-exception", "failed to get image from uri=$uri", e.message)
|
||||||
|
} finally {
|
||||||
|
Glide.with(activity).clear(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun streamBytes(inputStream: InputStream) {
|
||||||
|
val buffer = ByteArray(bufferSize)
|
||||||
|
var len: Int
|
||||||
|
while (inputStream.read(buffer).also { len = it } != -1) {
|
||||||
|
// cannot decode image on Flutter side when using `buffer` directly
|
||||||
|
success(buffer.copyOf(len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CHANNEL = "deckers.thibault/aves/imagebytestream"
|
||||||
|
|
||||||
|
const val bufferSize = 2 shl 17 // 256kB
|
||||||
|
|
||||||
|
// request a fresh image with the highest quality format
|
||||||
|
val options = RequestOptions()
|
||||||
|
.format(DecodeFormat.PREFER_ARGB_8888)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : EventChannel.StreamHandler {
|
class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : EventChannel.StreamHandler {
|
||||||
private lateinit var eventSink: EventSink
|
private lateinit var eventSink: EventSink
|
||||||
private lateinit var handler: Handler
|
private lateinit var handler: Handler
|
||||||
|
|
||||||
private var knownEntries: Map<Int, Int?>? = null
|
private var knownEntries: Map<Int, Int?>? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -53,10 +53,10 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
|
||||||
if (picture == null) {
|
if (picture == null) {
|
||||||
// not ideal: bitmap -> byte[] -> bitmap
|
// not ideal: bitmap -> byte[] -> bitmap
|
||||||
// but simple fallback and we cache result
|
// but simple fallback and we cache result
|
||||||
val bos = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
val bitmap = retriever.frameAtTime
|
val bitmap = retriever.frameAtTime
|
||||||
bitmap?.compress(Bitmap.CompressFormat.PNG, 0, bos)
|
bitmap?.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
||||||
picture = bos.toByteArray()
|
picture = stream.toByteArray()
|
||||||
}
|
}
|
||||||
callback.onDataReady(ByteArrayInputStream(picture))
|
callback.onDataReady(ByteArrayInputStream(picture))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -22,7 +22,6 @@ object PermissionManager {
|
||||||
// permission request code to pending runnable
|
// permission request code to pending runnable
|
||||||
private val pendingPermissionMap = ConcurrentHashMap<Int, PendingPermissionHandler>()
|
private val pendingPermissionMap = ConcurrentHashMap<Int, PendingPermissionHandler>()
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun requestVolumeAccess(activity: Activity, path: String, onGranted: () -> Unit, onDenied: () -> Unit) {
|
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")
|
Log.i(LOG_TAG, "request user to select and grant access permission to volume=$path")
|
||||||
pendingPermissionMap[VOLUME_ACCESS_REQUEST_CODE] = PendingPermissionHandler(path, onGranted, onDenied)
|
pendingPermissionMap[VOLUME_ACCESS_REQUEST_CODE] = PendingPermissionHandler(path, onGranted, onDenied)
|
||||||
|
@ -47,12 +46,10 @@ object PermissionManager {
|
||||||
(if (treeUri != null) handler.onGranted else handler.onDenied)()
|
(if (treeUri != null) handler.onGranted else handler.onDenied)()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getGrantedDirForPath(context: Context, anyPath: String): String? {
|
fun getGrantedDirForPath(context: Context, anyPath: String): String? {
|
||||||
return getAccessibleDirs(context).firstOrNull { anyPath.startsWith(it) }
|
return getAccessibleDirs(context).firstOrNull { anyPath.startsWith(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getInaccessibleDirectories(context: Context, dirPaths: List<String>): List<Map<String, String>> {
|
fun getInaccessibleDirectories(context: Context, dirPaths: List<String>): List<Map<String, String>> {
|
||||||
val accessibleDirs = getAccessibleDirs(context)
|
val accessibleDirs = getAccessibleDirs(context)
|
||||||
|
|
||||||
|
@ -103,16 +100,15 @@ object PermissionManager {
|
||||||
return inaccessibleDirs
|
return inaccessibleDirs
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
fun revokeDirectoryAccess(context: Context, path: String): Boolean {
|
||||||
fun revokeDirectoryAccess(context: Context, path: String) {
|
return StorageUtils.convertDirPathToTreeUri(context, path)?.let {
|
||||||
StorageUtils.convertDirPathToTreeUri(context, path)?.let {
|
|
||||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
context.contentResolver.releasePersistableUriPermission(it, flags)
|
context.contentResolver.releasePersistableUriPermission(it, flags)
|
||||||
}
|
true
|
||||||
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns paths matching URIs granted by the user
|
// returns paths matching URIs granted by the user
|
||||||
@JvmStatic
|
|
||||||
fun getGrantedDirs(context: Context): Set<String> {
|
fun getGrantedDirs(context: Context): Set<String> {
|
||||||
val grantedDirs = HashSet<String>()
|
val grantedDirs = HashSet<String>()
|
||||||
for (uriPermission in context.contentResolver.persistedUriPermissions) {
|
for (uriPermission in context.contentResolver.persistedUriPermissions) {
|
||||||
|
|
Loading…
Reference in a new issue