improved package retrieval

This commit is contained in:
Thibault Deckers 2021-01-30 12:38:35 +09:00
parent 8e44d4a9d9
commit f133ebf624
17 changed files with 158 additions and 79 deletions

View file

@ -40,7 +40,6 @@
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>

View file

@ -13,6 +13,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
@ -29,7 +30,7 @@ import kotlin.math.roundToInt
class AppAdapterHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getAppNames" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppNames) }
"getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) }
"getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppIcon) }
"edit" -> {
val title = call.argument<String>("title")
@ -62,30 +63,24 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
}
private fun getAppNames(@Suppress("UNUSED_PARAMETER") call: MethodCall, 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)
private fun getPackages(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
val packages = HashMap<String, FieldMap>()
fun addPackageDetails(intent: Intent) {
// 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) {
val appInfo = resolveInfo.activityInfo.applicationInfo
val packageName = appInfo.packageName
if (!packages.containsKey(packageName)) {
val currentLabel = pm.getApplicationLabel(appInfo).toString()
val englishLabel: String? = appInfo.labelRes.takeIf { it != 0 }?.let { labelRes ->
var englishLabel: String? = null
try {
val resources = pm.getResourcesForApplication(ai)
val resources = pm.getResourcesForApplication(appInfo)
// `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:
@ -93,15 +88,26 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// - 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
englishLabel = resources.getString(labelRes)
} catch (e: PackageManager.NameNotFoundException) {
Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e)
}
englishLabel
}
packages[packageName] = hashMapOf(
"packageName" to packageName,
"categoryLauncher" to intent.hasCategory(Intent.CATEGORY_LAUNCHER),
"isSystem" to (appInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0),
"currentLabel" to currentLabel,
"englishLabel" to englishLabel,
)
}
}
}
result.success(nameMap)
addPackageDetails(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER))
addPackageDetails(Intent(Intent.ACTION_MAIN))
result.success(ArrayList(packages.values))
}
private fun getAppIcon(call: MethodCall, result: MethodChannel.Result) {

View file

@ -16,7 +16,7 @@ import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.ExifInterfaceHelper
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.model.provider.FieldMap
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface

View file

@ -11,7 +11,7 @@ import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher
import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher
import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.model.provider.FieldMap
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.model.provider.MediaStoreImageProvider

View file

@ -46,7 +46,7 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
import deckers.thibault.aves.metadata.XMP.isPanorama
import deckers.thibault.aves.model.provider.FieldMap
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.FileImageProvider
import deckers.thibault.aves.model.provider.ImageProvider
import deckers.thibault.aves.utils.BitmapUtils

View file

@ -6,7 +6,7 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.provider.FieldMap
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.utils.LogUtils

View file

@ -4,7 +4,7 @@ import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import deckers.thibault.aves.model.provider.FieldMap
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.MediaStoreImageProvider
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.EventChannel

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.model
import android.net.Uri
import deckers.thibault.aves.model.provider.FieldMap
import deckers.thibault.aves.model.FieldMap
class AvesEntry(map: FieldMap) {
val uri: Uri = Uri.parse(map["uri"] as String) // content or file URI

View file

@ -0,0 +1,3 @@
package deckers.thibault.aves.model
typealias FieldMap = MutableMap<String, Any?>

View file

@ -25,7 +25,7 @@ import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong
import deckers.thibault.aves.model.provider.FieldMap
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import org.beyka.tiffbitmapfactory.TiffBitmapFactory

View file

@ -18,6 +18,7 @@ import deckers.thibault.aves.decoder.MultiTrackImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
@ -348,5 +349,3 @@ abstract class ImageProvider {
private val LOG_TAG = LogUtils.createTag(ImageProvider::class.java)
}
}
typealias FieldMap = MutableMap<String, Any?>

View file

@ -9,6 +9,7 @@ import android.provider.MediaStore
import android.util.Log
import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes

View file

@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:aves/model/entry.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@ -8,12 +9,18 @@ import 'package:flutter/services.dart';
class AndroidAppService {
static const platform = MethodChannel('deckers.thibault/aves/app');
static Future<Map> getAppNames() async {
static Future<Set<Package>> getPackages() async {
try {
final result = await platform.invokeMethod('getAppNames');
return result as Map;
final result = await platform.invokeMethod('getPackages');
final packages = (result as List).cast<Map>().map((map) => Package.fromMap(map)).toSet();
// additional info for known directories
final kakaoTalk = packages.firstWhere((package) => package.packageName == 'com.kakao.talk', orElse: () => null);
if (kakaoTalk != null) {
kakaoTalk.ownedDirs.add('KakaoTalkDownload');
}
return packages;
} on PlatformException catch (e) {
debugPrint('getAppNames failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
debugPrint('getPackages failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
}
return {};
}

View file

@ -9,14 +9,14 @@ class AndroidFileService {
static const platform = MethodChannel('deckers.thibault/aves/storage');
static final StreamsChannel storageAccessChannel = StreamsChannel('deckers.thibault/aves/storageaccessstream');
static Future<List<Map>> getStorageVolumes() async {
static Future<Set<StorageVolume>> getStorageVolumes() async {
try {
final result = await platform.invokeMethod('getStorageVolumes');
return (result as List).cast<Map>();
return (result as List).cast<Map>().map((map) => StorageVolume.fromMap(map)).toSet();
} on PlatformException catch (e) {
debugPrint('getStorageVolumes failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
}
return [];
return {};
}
static Future<int> getFreeSpace(StorageVolume volume) async {

View file

@ -1,6 +1,7 @@
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/android_file_service.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
@ -8,14 +9,17 @@ final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
class AndroidFileUtils {
String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath;
Set<StorageVolume> storageVolumes = {};
Map _installedAppNameMap = {};
Set<Package> _packages = {};
List<String> _potentialAppDirs = [];
AChangeNotifier appNameChangeNotifier = AChangeNotifier();
Iterable<Package> get _launcherPackages => _packages.where((package) => package.categoryLauncher);
AndroidFileUtils._private();
Future<void> init() async {
storageVolumes = (await AndroidFileService.getStorageVolumes()).map((map) => StorageVolume.fromMap(map)).toSet();
storageVolumes = await AndroidFileService.getStorageVolumes();
// path_provider getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files'
primaryStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path;
dcimPath = join(primaryStorage, 'DCIM');
@ -25,8 +29,8 @@ class AndroidFileUtils {
}
Future<void> initAppNames() async {
_installedAppNameMap = await AndroidAppService.getAppNames()
..addAll({'KakaoTalkDownload': 'com.kakao.talk'});
_packages = await AndroidAppService.getPackages();
_potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList();
appNameChangeNotifier.notifyListeners();
}
@ -42,28 +46,67 @@ class AndroidFileUtils {
bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false;
AlbumType getAlbumType(String albumDirectory) {
if (albumDirectory != null) {
if (isCameraPath(albumDirectory)) return AlbumType.camera;
if (isDownloadPath(albumDirectory)) return AlbumType.download;
if (isScreenRecordingsPath(albumDirectory)) return AlbumType.screenRecordings;
if (isScreenshotsPath(albumDirectory)) return AlbumType.screenshots;
AlbumType getAlbumType(String albumPath) {
if (albumPath != null) {
if (isCameraPath(albumPath)) return AlbumType.camera;
if (isDownloadPath(albumPath)) return AlbumType.download;
if (isScreenRecordingsPath(albumPath)) return AlbumType.screenRecordings;
if (isScreenshotsPath(albumPath)) return AlbumType.screenshots;
final parts = albumDirectory.split(separator);
if (albumDirectory.startsWith(primaryStorage) && _isInstalledAppName(parts.last)) return AlbumType.app;
final dir = albumPath.split(separator).last;
if (albumPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app;
}
return AlbumType.regular;
}
bool _isInstalledAppName(String name) => _installedAppNameMap.keys.contains(name);
String getAlbumAppPackageName(String albumPath) {
if (albumPath == null) return null;
final dir = albumPath.split(separator).last;
final package = _launcherPackages.firstWhere((package) => package.potentialDirs.contains(dir), orElse: () => null);
return package?.packageName;
}
String getAlbumAppPackageName(String albumDirectory) => _installedAppNameMap[albumDirectory.split(separator).last];
String getAppName(String packageName) => _installedAppNameMap.entries.firstWhere((kv) => kv.value == packageName, orElse: () => null)?.key;
String getCurrentAppName(String packageName) {
final package = _packages.firstWhere((package) => package.packageName == packageName, orElse: () => null);
return package?.currentLabel;
}
}
enum AlbumType { regular, app, camera, download, screenRecordings, screenshots }
class Package {
final String packageName, currentLabel, englishLabel;
final bool categoryLauncher, isSystem;
final Set<String> ownedDirs = {};
Package({
this.packageName,
this.currentLabel,
this.englishLabel,
this.categoryLauncher,
this.isSystem,
});
factory Package.fromMap(Map map) {
return Package(
packageName: map['packageName'],
currentLabel: map['currentLabel'],
englishLabel: map['englishLabel'],
categoryLauncher: map['categoryLauncher'],
isSystem: map['isSystem'],
);
}
Set<String> get potentialDirs => [
currentLabel,
englishLabel,
...ownedDirs,
].where((dir) => dir != null).toSet();
@override
String toString() => '$runtimeType#${shortHash(this)}{packageName=$packageName, categoryLauncher=$categoryLauncher, isSystem=$isSystem, currentLabel=$currentLabel, englishLabel=$englishLabel, ownedDirs=$ownedDirs}';
}
class StorageVolume {
final String description, path, state;
final bool isEmulated, isPrimary, isRemovable;

View file

@ -1,5 +1,6 @@
import 'package:aves/image_providers/app_icon_image_provider.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/viewer/info/common.dart';
import 'package:collection/collection.dart';
@ -11,14 +12,14 @@ class DebugAndroidAppSection extends StatefulWidget {
}
class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with AutomaticKeepAliveClientMixin {
Future<Map> _loader;
Future<Set<Package>> _loader;
static const iconSize = 20.0;
@override
void initState() {
super.initState();
_loader = AndroidAppService.getAppNames();
_loader = AndroidAppService.getPackages();
}
@override
@ -30,17 +31,17 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with Au
children: [
Padding(
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: FutureBuilder<Map>(
child: FutureBuilder<Set<Package>>(
future: _loader,
builder: (context, snapshot) {
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
final entries = snapshot.data.entries.toList()..sort((kv1, kv2) => compareAsciiUpperCase(kv1.value, kv2.value));
final packages = snapshot.data.toList()..sort((a, b) => compareAsciiUpperCase(a.packageName, b.packageName));
final enabledTheme = IconTheme.of(context);
final disabledTheme = enabledTheme.merge(IconThemeData(opacity: .2));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: entries.map((kv) {
final appName = kv.key.toString();
final packageName = kv.value.toString();
children: packages.map((package) {
return Text.rich(
TextSpan(
children: [
@ -48,7 +49,7 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with Au
alignment: PlaceholderAlignment.middle,
child: Image(
image: AppIconImage(
packageName: packageName,
packageName: package.packageName,
size: iconSize,
),
width: iconSize,
@ -56,11 +57,31 @@ class _DebugAndroidAppSectionState extends State<DebugAndroidAppSection> with Au
),
),
TextSpan(
text: ' $packageName',
text: ' ${package.packageName}\n',
style: InfoRowGroup.keyStyle,
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: IconTheme(
data: package.categoryLauncher ? enabledTheme : disabledTheme,
child: Icon(
Icons.launch_outlined,
size: iconSize,
),
),
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: IconTheme(
data: package.isSystem ? enabledTheme : disabledTheme,
child: Icon(
Icons.android,
size: iconSize,
),
),
),
TextSpan(
text: ' $appName',
text: ' ${package.potentialDirs.join(', ')}\n',
style: InfoRowGroup.baseStyle,
),
],

View file

@ -136,7 +136,7 @@ class _OwnerPropState extends State<OwnerProp> {
builder: (context, snapshot) {
final packageName = snapshot.data;
if (packageName == null) return SizedBox();
final appName = androidFileUtils.getAppName(packageName) ?? packageName;
final appName = androidFileUtils.getCurrentAppName(packageName) ?? packageName;
return Text.rich(
TextSpan(
children: [