Kotlin migration (WIP)

This commit is contained in:
Thibault Deckers 2020-10-20 17:51:21 +09:00
parent db54c4cf9c
commit e50dd952a8
10 changed files with 364 additions and 415 deletions

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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