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";
|
||||
|
||||
private Context context;
|
||||
private final Context context;
|
||||
private EventChannel.EventSink eventSink;
|
||||
private Handler handler;
|
||||
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) {
|
||||
val treeUri = data.data
|
||||
val treeUri = data?.data
|
||||
if (resultCode != RESULT_OK || treeUri == null) {
|
||||
PermissionManager.onPermissionResult(requestCode, null)
|
||||
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
|
||||
if (uriList.size == 1) {
|
||||
return shareSingle(title, uriList[0], mimeTypes[0])
|
||||
return shareSingle(title, uriList.first(), mimeTypes.first())
|
||||
}
|
||||
|
||||
var mimeType = "*/*"
|
||||
if (mimeTypes.size == 1) {
|
||||
// items have the same mime type & subtype
|
||||
mimeType = mimeTypes[0]
|
||||
mimeType = mimeTypes.first()
|
||||
} else {
|
||||
// items have different subtypes
|
||||
val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct()
|
||||
if (mimeTypeTypes.size == 1) {
|
||||
// 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 {
|
||||
private lateinit var eventSink: EventSink
|
||||
private lateinit var handler: Handler
|
||||
|
||||
private var knownEntries: Map<Int, Int?>? = null
|
||||
|
||||
init {
|
||||
|
|
|
@ -53,10 +53,10 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
|
|||
if (picture == null) {
|
||||
// not ideal: bitmap -> byte[] -> bitmap
|
||||
// but simple fallback and we cache result
|
||||
val bos = ByteArrayOutputStream()
|
||||
val stream = ByteArrayOutputStream()
|
||||
val bitmap = retriever.frameAtTime
|
||||
bitmap?.compress(Bitmap.CompressFormat.PNG, 0, bos)
|
||||
picture = bos.toByteArray()
|
||||
bitmap?.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
||||
picture = stream.toByteArray()
|
||||
}
|
||||
callback.onDataReady(ByteArrayInputStream(picture))
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -22,7 +22,6 @@ object PermissionManager {
|
|||
// permission request code to pending runnable
|
||||
private val pendingPermissionMap = ConcurrentHashMap<Int, PendingPermissionHandler>()
|
||||
|
||||
@JvmStatic
|
||||
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")
|
||||
pendingPermissionMap[VOLUME_ACCESS_REQUEST_CODE] = PendingPermissionHandler(path, onGranted, onDenied)
|
||||
|
@ -47,12 +46,10 @@ object PermissionManager {
|
|||
(if (treeUri != null) handler.onGranted else handler.onDenied)()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getGrantedDirForPath(context: Context, anyPath: String): String? {
|
||||
return getAccessibleDirs(context).firstOrNull { anyPath.startsWith(it) }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getInaccessibleDirectories(context: Context, dirPaths: List<String>): List<Map<String, String>> {
|
||||
val accessibleDirs = getAccessibleDirs(context)
|
||||
|
||||
|
@ -103,16 +100,15 @@ object PermissionManager {
|
|||
return inaccessibleDirs
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun revokeDirectoryAccess(context: Context, path: String) {
|
||||
StorageUtils.convertDirPathToTreeUri(context, path)?.let {
|
||||
fun revokeDirectoryAccess(context: Context, path: String): Boolean {
|
||||
return StorageUtils.convertDirPathToTreeUri(context, path)?.let {
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
context.contentResolver.releasePersistableUriPermission(it, flags)
|
||||
}
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
// returns paths matching URIs granted by the user
|
||||
@JvmStatic
|
||||
fun getGrantedDirs(context: Context): Set<String> {
|
||||
val grantedDirs = HashSet<String>()
|
||||
for (uriPermission in context.contentResolver.persistedUriPermissions) {
|
||||
|
|
Loading…
Reference in a new issue