Kotlin migration (WIP)
This commit is contained in:
parent
db54c4cf9c
commit
e50dd952a8
10 changed files with 364 additions and 415 deletions
|
@ -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<String, List<String>> urisByMimeType = call.argument("urisByMimeType");
|
|
||||||
shareMultiple(title, urisByMimeType);
|
|
||||||
result.success(null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
result.notImplemented();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, String> getAppNames() {
|
|
||||||
Map<String, String> 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<ResolveInfo> 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<Bitmap> 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<String, String> 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<String, List<String>> urisByMimeType) {
|
|
||||||
if (urisByMimeType == null) return;
|
|
||||||
|
|
||||||
ArrayList<Uri> 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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<String> 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<String> 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -70,7 +70,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private Activity activity;
|
private final Activity activity;
|
||||||
|
|
||||||
ImageDecodeTask(Activity activity) {
|
ImageDecodeTask(Activity activity) {
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
|
|
|
@ -24,7 +24,7 @@ import io.flutter.plugin.common.MethodChannel;
|
||||||
public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||||
public static final String CHANNEL = "deckers.thibault/aves/image";
|
public static final String CHANNEL = "deckers.thibault/aves/image";
|
||||||
|
|
||||||
private Activity activity;
|
private final Activity activity;
|
||||||
private float density;
|
private float density;
|
||||||
|
|
||||||
public ImageFileHandler(Activity activity) {
|
public ImageFileHandler(Activity activity) {
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,255 @@
|
||||||
|
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.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import deckers.thibault.aves.utils.LogUtils.createTag
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
when (call.method) {
|
||||||
|
"getAppIcon" -> Thread { getAppIcon(call, MethodResultWrapper(result)) }.start()
|
||||||
|
"getAppNames" -> Thread { getAppNames(MethodResultWrapper(result)) }.start()
|
||||||
|
"getEnv" -> result.success(System.getenv())
|
||||||
|
"edit" -> {
|
||||||
|
val title = call.argument<String>("title")
|
||||||
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
val mimeType = call.argument<String>("mimeType")
|
||||||
|
result.success(edit(title, uri, mimeType))
|
||||||
|
}
|
||||||
|
"open" -> {
|
||||||
|
val title = call.argument<String>("title")
|
||||||
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
val mimeType = call.argument<String>("mimeType")
|
||||||
|
result.success(open(title, uri, mimeType))
|
||||||
|
}
|
||||||
|
"openMap" -> {
|
||||||
|
val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) }
|
||||||
|
result.success(openMap(geoUri))
|
||||||
|
}
|
||||||
|
"setAs" -> {
|
||||||
|
val title = call.argument<String>("title")
|
||||||
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
val mimeType = call.argument<String>("mimeType")
|
||||||
|
result.success(setAs(title, uri, mimeType))
|
||||||
|
}
|
||||||
|
"share" -> {
|
||||||
|
val title = call.argument<String>("title")
|
||||||
|
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")!!
|
||||||
|
result.success(shareMultiple(title, urisByMimeType))
|
||||||
|
}
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAppNames(result: MethodChannel.Result) {
|
||||||
|
val nameMap = HashMap<String, String>()
|
||||||
|
val intent = Intent(Intent.ACTION_MAIN, null)
|
||||||
|
.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or 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
|
||||||
|
val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) }
|
||||||
|
|
||||||
|
val pm = context.packageManager
|
||||||
|
for (resolveInfo in pm.queryIntentActivities(intent, 0)) {
|
||||||
|
val ai = resolveInfo.activityInfo.applicationInfo
|
||||||
|
val isSystemPackage = ai.flags and ApplicationInfo.FLAG_SYSTEM != 0
|
||||||
|
if (!isSystemPackage) {
|
||||||
|
val packageName = ai.packageName
|
||||||
|
|
||||||
|
val currentLabel = pm.getApplicationLabel(ai).toString()
|
||||||
|
nameMap[currentLabel] = packageName
|
||||||
|
|
||||||
|
val labelRes = ai.labelRes
|
||||||
|
if (labelRes != 0) {
|
||||||
|
try {
|
||||||
|
val 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)`
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
resources.updateConfiguration(englishConfig, resources.displayMetrics)
|
||||||
|
val englishLabel = resources.getString(labelRes)
|
||||||
|
nameMap[englishLabel] = packageName
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.success(nameMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAppIcon(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val packageName = call.argument<String>("packageName")
|
||||||
|
val sizeDip = call.argument<Double>("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
|
||||||
|
val density = context.resources.displayMetrics.density
|
||||||
|
val size = (sizeDip * density).roundToInt()
|
||||||
|
var data: ByteArray? = null
|
||||||
|
try {
|
||||||
|
val iconResourceId = context.packageManager.getApplicationInfo(packageName, 0).icon
|
||||||
|
val uri = Uri.Builder()
|
||||||
|
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||||
|
.authority(packageName)
|
||||||
|
.path(iconResourceId.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val options = RequestOptions()
|
||||||
|
.format(DecodeFormat.PREFER_RGB_565)
|
||||||
|
.centerCrop()
|
||||||
|
.override(size, size)
|
||||||
|
val target = Glide.with(context)
|
||||||
|
.asBitmap()
|
||||||
|
.apply(options)
|
||||||
|
.load(uri)
|
||||||
|
.submit(size, size)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val bitmap = target.get()
|
||||||
|
if (bitmap != null) {
|
||||||
|
val stream = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
||||||
|
data = stream.toByteArray()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e)
|
||||||
|
}
|
||||||
|
Glide.with(context).clear(target)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
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 fun edit(title: String?, uri: Uri?, mimeType: String?): Boolean {
|
||||||
|
uri ?: return false
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_EDIT)
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
.setDataAndType(uri, mimeType)
|
||||||
|
return safeStartActivityChooser(title, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun open(title: String?, uri: Uri?, mimeType: String?): Boolean {
|
||||||
|
uri ?: return false
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
.setDataAndType(uri, mimeType)
|
||||||
|
return safeStartActivityChooser(title, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openMap(geoUri: Uri?): Boolean {
|
||||||
|
geoUri ?: return false
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, geoUri)
|
||||||
|
return safeStartActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAs(title: String?, uri: Uri?, mimeType: String?): Boolean {
|
||||||
|
uri ?: return false
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_ATTACH_DATA)
|
||||||
|
.setDataAndType(uri, mimeType)
|
||||||
|
return safeStartActivityChooser(title, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shareSingle(title: String?, uri: Uri, mimeType: String): Boolean {
|
||||||
|
val intent = Intent(Intent.ACTION_SEND)
|
||||||
|
.setType(mimeType)
|
||||||
|
when (uri.scheme?.toLowerCase(Locale.ROOT)) {
|
||||||
|
ContentResolver.SCHEME_FILE -> {
|
||||||
|
val path = uri.path ?: return false
|
||||||
|
val applicationId = context.applicationContext.packageName
|
||||||
|
val apkUri = FileProvider.getUriForFile(context, "$applicationId.fileprovider", File(path))
|
||||||
|
intent.putExtra(Intent.EXTRA_STREAM, apkUri)
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
else -> intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
}
|
||||||
|
return safeStartActivityChooser(title, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shareMultiple(title: String?, urisByMimeType: Map<String, List<String>>?): Boolean {
|
||||||
|
urisByMimeType ?: return false
|
||||||
|
|
||||||
|
val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { Uri.parse(it) })
|
||||||
|
val mimeTypes = urisByMimeType.keys.toTypedArray()
|
||||||
|
|
||||||
|
// 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])
|
||||||
|
}
|
||||||
|
|
||||||
|
var mimeType = "*/*"
|
||||||
|
if (mimeTypes.size == 1) {
|
||||||
|
// items have the same mime type & subtype
|
||||||
|
mimeType = mimeTypes[0]
|
||||||
|
} 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() + "/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_SEND_MULTIPLE)
|
||||||
|
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
|
||||||
|
.setType(mimeType)
|
||||||
|
return safeStartActivityChooser(title, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun safeStartActivity(intent: Intent): Boolean {
|
||||||
|
val canResolve = intent.resolveActivity(context.packageManager) != null
|
||||||
|
if (canResolve) {
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
return canResolve
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun safeStartActivityChooser(title: String?, intent: Intent): Boolean {
|
||||||
|
val canResolve = intent.resolveActivity(context.packageManager) != null
|
||||||
|
if (canResolve) {
|
||||||
|
context.startActivity(Intent.createChooser(intent, title))
|
||||||
|
}
|
||||||
|
return canResolve
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val LOG_TAG = createTag(AppAdapterHandler::class.java)
|
||||||
|
const val CHANNEL = "deckers.thibault/aves/app"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package deckers.thibault.aves.channel.calls
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import deckers.thibault.aves.MainActivity
|
||||||
|
import deckers.thibault.aves.R
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils.centerSquareCrop
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
|
||||||
|
class AppShortcutHandler(private val context: Context) : MethodCallHandler {
|
||||||
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
when (call.method) {
|
||||||
|
"canPin" -> result.success(canPin())
|
||||||
|
"pin" -> {
|
||||||
|
Thread { pin(call) }.start()
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun canPin() = ShortcutManagerCompat.isRequestPinShortcutSupported(context)
|
||||||
|
|
||||||
|
private fun pin(call: MethodCall) {
|
||||||
|
if (!canPin()) return
|
||||||
|
|
||||||
|
val label = call.argument<String>("label") ?: return
|
||||||
|
val iconBytes = call.argument<ByteArray>("iconBytes")
|
||||||
|
val filters = call.argument<List<String?>>("filters") ?: return
|
||||||
|
|
||||||
|
var icon: IconCompat? = null
|
||||||
|
if (iconBytes?.isNotEmpty() == true) {
|
||||||
|
var bitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
|
||||||
|
bitmap = centerSquareCrop(context, bitmap, 256)
|
||||||
|
if (bitmap != null) {
|
||||||
|
icon = IconCompat.createWithBitmap(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (icon == null) {
|
||||||
|
icon = IconCompat.createWithResource(context, R.mipmap.ic_shortcut_collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
|
||||||
|
.putExtra("page", "/collection")
|
||||||
|
.putExtra("filters", filters.toTypedArray())
|
||||||
|
|
||||||
|
val shortcut = ShortcutInfoCompat.Builder(context, "collection-${filters.joinToString("-")}")
|
||||||
|
.setShortLabel(label)
|
||||||
|
.setIcon(icon)
|
||||||
|
.setIntent(intent)
|
||||||
|
.build()
|
||||||
|
ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CHANNEL = "deckers.thibault/aves/shortcut"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
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
|
||||||
|
class MethodResultWrapper internal constructor(private val methodResult: MethodChannel.Result) : MethodChannel.Result {
|
||||||
|
private val handler: Handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
override fun success(result: Any?) {
|
||||||
|
handler.post { methodResult.success(result) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||||
|
handler.post { methodResult.error(errorCode, errorMessage, errorDetails) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notImplemented() {
|
||||||
|
handler.post { methodResult.notImplemented() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ object ExifInterfaceHelper {
|
||||||
ExifInterface.TAG_ORIENTATION,
|
ExifInterface.TAG_ORIENTATION,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val baseTags: Map<String, TagMapper?> = hashMapOf(
|
private val baseTags: Map<String, TagMapper?> = mapOf(
|
||||||
ExifInterface.TAG_APERTURE_VALUE to TagMapper(ExifDirectoryBase.TAG_APERTURE, DirType.EXIF_IFD0, TagFormat.RATIONAL),
|
ExifInterface.TAG_APERTURE_VALUE to TagMapper(ExifDirectoryBase.TAG_APERTURE, DirType.EXIF_IFD0, TagFormat.RATIONAL),
|
||||||
ExifInterface.TAG_ARTIST to TagMapper(ExifDirectoryBase.TAG_ARTIST, DirType.EXIF_IFD0, TagFormat.ASCII),
|
ExifInterface.TAG_ARTIST to TagMapper(ExifDirectoryBase.TAG_ARTIST, DirType.EXIF_IFD0, TagFormat.ASCII),
|
||||||
ExifInterface.TAG_BITS_PER_SAMPLE to TagMapper(ExifDirectoryBase.TAG_BITS_PER_SAMPLE, DirType.EXIF_IFD0, TagFormat.SHORT),
|
ExifInterface.TAG_BITS_PER_SAMPLE to TagMapper(ExifDirectoryBase.TAG_BITS_PER_SAMPLE, DirType.EXIF_IFD0, TagFormat.SHORT),
|
||||||
|
@ -133,12 +133,12 @@ object ExifInterfaceHelper {
|
||||||
ExifInterface.TAG_Y_RESOLUTION to TagMapper(ExifDirectoryBase.TAG_Y_RESOLUTION, DirType.EXIF_IFD0, TagFormat.RATIONAL),
|
ExifInterface.TAG_Y_RESOLUTION to TagMapper(ExifDirectoryBase.TAG_Y_RESOLUTION, DirType.EXIF_IFD0, TagFormat.RATIONAL),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val thumbnailTags: Map<String, TagMapper?> = hashMapOf(
|
private val thumbnailTags: Map<String, TagMapper?> = mapOf(
|
||||||
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT to TagMapper(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET, DirType.EXIF_THUMBNAIL, TagFormat.LONG), // IFD_TIFF_TAGS or IFD_THUMBNAIL_TAGS 0x0201
|
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT to TagMapper(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET, DirType.EXIF_THUMBNAIL, TagFormat.LONG), // IFD_TIFF_TAGS or IFD_THUMBNAIL_TAGS 0x0201
|
||||||
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH to TagMapper(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH, DirType.EXIF_THUMBNAIL, TagFormat.LONG), // IFD_TIFF_TAGS or IFD_THUMBNAIL_TAGS 0x0202
|
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH to TagMapper(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH, DirType.EXIF_THUMBNAIL, TagFormat.LONG), // IFD_TIFF_TAGS or IFD_THUMBNAIL_TAGS 0x0202
|
||||||
)
|
)
|
||||||
|
|
||||||
private val gpsTags: Map<String, TagMapper?> = hashMapOf(
|
private val gpsTags: Map<String, TagMapper?> = mapOf(
|
||||||
ExifInterface.TAG_GPS_ALTITUDE to TagMapper(GpsDirectory.TAG_ALTITUDE, DirType.GPS, TagFormat.RATIONAL),
|
ExifInterface.TAG_GPS_ALTITUDE to TagMapper(GpsDirectory.TAG_ALTITUDE, DirType.GPS, TagFormat.RATIONAL),
|
||||||
ExifInterface.TAG_GPS_ALTITUDE_REF to TagMapper(GpsDirectory.TAG_ALTITUDE_REF, DirType.GPS, TagFormat.BYTE),
|
ExifInterface.TAG_GPS_ALTITUDE_REF to TagMapper(GpsDirectory.TAG_ALTITUDE_REF, DirType.GPS, TagFormat.BYTE),
|
||||||
ExifInterface.TAG_GPS_AREA_INFORMATION to TagMapper(GpsDirectory.TAG_AREA_INFORMATION, DirType.GPS, TagFormat.COMMENT),
|
ExifInterface.TAG_GPS_AREA_INFORMATION to TagMapper(GpsDirectory.TAG_AREA_INFORMATION, DirType.GPS, TagFormat.COMMENT),
|
||||||
|
@ -173,11 +173,11 @@ object ExifInterfaceHelper {
|
||||||
ExifInterface.TAG_GPS_VERSION_ID to TagMapper(GpsDirectory.TAG_VERSION_ID, DirType.GPS, TagFormat.BYTE),
|
ExifInterface.TAG_GPS_VERSION_ID to TagMapper(GpsDirectory.TAG_VERSION_ID, DirType.GPS, TagFormat.BYTE),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val xmpTags: Map<String, TagMapper?> = hashMapOf(
|
private val xmpTags: Map<String, TagMapper?> = mapOf(
|
||||||
ExifInterface.TAG_XMP to null, // IFD_TIFF_TAGS 0x02BC
|
ExifInterface.TAG_XMP to null, // IFD_TIFF_TAGS 0x02BC
|
||||||
)
|
)
|
||||||
|
|
||||||
private val rawTags: Map<String, TagMapper?> = hashMapOf(
|
private val rawTags: Map<String, TagMapper?> = mapOf(
|
||||||
// DNG
|
// DNG
|
||||||
ExifInterface.TAG_DEFAULT_CROP_SIZE to null, // IFD_EXIF_TAGS 0xC620
|
ExifInterface.TAG_DEFAULT_CROP_SIZE to null, // IFD_EXIF_TAGS 0xC620
|
||||||
ExifInterface.TAG_DNG_VERSION to null, // IFD_EXIF_TAGS 0xC612
|
ExifInterface.TAG_DNG_VERSION to null, // IFD_EXIF_TAGS 0xC612
|
||||||
|
|
|
@ -41,9 +41,9 @@ class AndroidAppService {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> edit(String uri, String mimeType) async {
|
static Future<bool> edit(String uri, String mimeType) async {
|
||||||
try {
|
try {
|
||||||
await platform.invokeMethod('edit', <String, dynamic>{
|
return await platform.invokeMethod('edit', <String, dynamic>{
|
||||||
'title': 'Edit with:',
|
'title': 'Edit with:',
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
|
@ -51,11 +51,12 @@ class AndroidAppService {
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('edit failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
debugPrint('edit failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> open(String uri, String mimeType) async {
|
static Future<bool> open(String uri, String mimeType) async {
|
||||||
try {
|
try {
|
||||||
await platform.invokeMethod('open', <String, dynamic>{
|
return await platform.invokeMethod('open', <String, dynamic>{
|
||||||
'title': 'Open with:',
|
'title': 'Open with:',
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
|
@ -63,22 +64,23 @@ class AndroidAppService {
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('open failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
debugPrint('open failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> openMap(String geoUri) async {
|
static Future<bool> openMap(String geoUri) async {
|
||||||
if (geoUri == null) return;
|
|
||||||
try {
|
try {
|
||||||
await platform.invokeMethod('openMap', <String, dynamic>{
|
return await platform.invokeMethod('openMap', <String, dynamic>{
|
||||||
'geoUri': geoUri,
|
'geoUri': geoUri,
|
||||||
});
|
});
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('openMap failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
debugPrint('openMap failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setAs(String uri, String mimeType) async {
|
static Future<bool> setAs(String uri, String mimeType) async {
|
||||||
try {
|
try {
|
||||||
await platform.invokeMethod('setAs', <String, dynamic>{
|
return await platform.invokeMethod('setAs', <String, dynamic>{
|
||||||
'title': 'Set as:',
|
'title': 'Set as:',
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
|
@ -86,19 +88,21 @@ class AndroidAppService {
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('setAs failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
debugPrint('setAs failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> share(Iterable<ImageEntry> entries) async {
|
static Future<bool> share(Iterable<ImageEntry> entries) async {
|
||||||
// loosen mime type to a generic one, so we can share with badly defined apps
|
// loosen mime type to a generic one, so we can share with badly defined apps
|
||||||
// e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats
|
// e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats
|
||||||
final urisByMimeType = groupBy<ImageEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
|
final urisByMimeType = groupBy<ImageEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
|
||||||
try {
|
try {
|
||||||
await platform.invokeMethod('share', <String, dynamic>{
|
return await platform.invokeMethod('share', <String, dynamic>{
|
||||||
'title': 'Share via:',
|
'title': 'Share via:',
|
||||||
'urisByMimeType': urisByMimeType,
|
'urisByMimeType': urisByMimeType,
|
||||||
});
|
});
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
debugPrint('share failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
debugPrint('share failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue