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")
|
||||
private Activity activity;
|
||||
private final Activity activity;
|
||||
|
||||
ImageDecodeTask(Activity activity) {
|
||||
this.activity = activity;
|
||||
|
|
|
@ -24,7 +24,7 @@ import io.flutter.plugin.common.MethodChannel;
|
|||
public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
||||
public static final String CHANNEL = "deckers.thibault/aves/image";
|
||||
|
||||
private Activity activity;
|
||||
private final Activity activity;
|
||||
private float density;
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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_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),
|
||||
|
@ -133,12 +133,12 @@ object ExifInterfaceHelper {
|
|||
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_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_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),
|
||||
|
@ -173,11 +173,11 @@ object ExifInterfaceHelper {
|
|||
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
|
||||
)
|
||||
|
||||
private val rawTags: Map<String, TagMapper?> = hashMapOf(
|
||||
private val rawTags: Map<String, TagMapper?> = mapOf(
|
||||
// DNG
|
||||
ExifInterface.TAG_DEFAULT_CROP_SIZE to null, // IFD_EXIF_TAGS 0xC620
|
||||
ExifInterface.TAG_DNG_VERSION to null, // IFD_EXIF_TAGS 0xC612
|
||||
|
|
|
@ -41,9 +41,9 @@ class AndroidAppService {
|
|||
return {};
|
||||
}
|
||||
|
||||
static Future<void> edit(String uri, String mimeType) async {
|
||||
static Future<bool> edit(String uri, String mimeType) async {
|
||||
try {
|
||||
await platform.invokeMethod('edit', <String, dynamic>{
|
||||
return await platform.invokeMethod('edit', <String, dynamic>{
|
||||
'title': 'Edit with:',
|
||||
'uri': uri,
|
||||
'mimeType': mimeType,
|
||||
|
@ -51,11 +51,12 @@ class AndroidAppService {
|
|||
} on PlatformException catch (e) {
|
||||
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 {
|
||||
await platform.invokeMethod('open', <String, dynamic>{
|
||||
return await platform.invokeMethod('open', <String, dynamic>{
|
||||
'title': 'Open with:',
|
||||
'uri': uri,
|
||||
'mimeType': mimeType,
|
||||
|
@ -63,22 +64,23 @@ class AndroidAppService {
|
|||
} on PlatformException catch (e) {
|
||||
debugPrint('open failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Future<void> openMap(String geoUri) async {
|
||||
if (geoUri == null) return;
|
||||
static Future<bool> openMap(String geoUri) async {
|
||||
try {
|
||||
await platform.invokeMethod('openMap', <String, dynamic>{
|
||||
return await platform.invokeMethod('openMap', <String, dynamic>{
|
||||
'geoUri': geoUri,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
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 {
|
||||
await platform.invokeMethod('setAs', <String, dynamic>{
|
||||
return await platform.invokeMethod('setAs', <String, dynamic>{
|
||||
'title': 'Set as:',
|
||||
'uri': uri,
|
||||
'mimeType': mimeType,
|
||||
|
@ -86,19 +88,21 @@ class AndroidAppService {
|
|||
} on PlatformException catch (e) {
|
||||
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
|
||||
// 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()));
|
||||
try {
|
||||
await platform.invokeMethod('share', <String, dynamic>{
|
||||
return await platform.invokeMethod('share', <String, dynamic>{
|
||||
'title': 'Share via:',
|
||||
'urisByMimeType': urisByMimeType,
|
||||
});
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('share failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue