From f133ebf624365eb095fc64c8d6894efa5357a657 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 30 Jan 2021 12:38:35 +0900 Subject: [PATCH] improved package retrieval --- android/app/src/main/AndroidManifest.xml | 1 - .../aves/channel/calls/AppAdapterHandler.kt | 76 ++++++++++--------- .../aves/channel/calls/DebugHandler.kt | 2 +- .../aves/channel/calls/ImageFileHandler.kt | 2 +- .../aves/channel/calls/MetadataHandler.kt | 2 +- .../channel/streams/ImageOpStreamHandler.kt | 2 +- .../streams/MediaStoreStreamHandler.kt | 2 +- .../deckers/thibault/aves/model/AvesEntry.kt | 2 +- .../deckers/thibault/aves/model/FieldMap.kt | 3 + .../thibault/aves/model/SourceEntry.kt | 2 +- .../aves/model/provider/ImageProvider.kt | 3 +- .../model/provider/MediaStoreImageProvider.kt | 1 + lib/services/android_app_service.dart | 15 +++- lib/services/android_file_service.dart | 6 +- lib/utils/android_file_utils.dart | 75 ++++++++++++++---- lib/widgets/debug/android_apps.dart | 41 +++++++--- lib/widgets/viewer/info/basic_section.dart | 2 +- 17 files changed, 158 insertions(+), 79 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1faac6cc8..66f30eaf6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -40,7 +40,6 @@ - diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 3e2ebda05..c4b8e3377 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -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("title") @@ -62,46 +63,51 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { } } - private fun getAppNames(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { - val nameMap = HashMap() - 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() - // 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) } + 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) { - 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) + val pm = context.packageManager + for (resolveInfo in pm.queryIntentActivities(intent, 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(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: + // - `resources.getConfiguration().setLocale(...)` + // - getting a package manager from a custom context with `context.createConfigurationContext(config)` + @Suppress("DEPRECATION") + resources.updateConfiguration(englishConfig, resources.displayMetrics) + 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) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt index 13ec93da6..6b4401073 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt @@ -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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt index 776747900..a9c831565 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt @@ -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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index a3f64479d..940ab0cba 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt index b70de372a..86e8d0650 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt @@ -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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt index f2892cfb2..ac9fec726 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt @@ -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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt index dfa0cbd32..872ed6819 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/AvesEntry.kt @@ -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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt new file mode 100644 index 000000000..78592a2b0 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/FieldMap.kt @@ -0,0 +1,3 @@ +package deckers.thibault.aves.model + +typealias FieldMap = MutableMap diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt index a482087ac..5a51905da 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt @@ -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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index 4ff6a1ff8..f92ba728c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index f06b5ea04..b87c55f49 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -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 diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 4f6b6750c..9a379334d 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -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 getAppNames() async { + static Future> 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) => 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 {}; } diff --git a/lib/services/android_file_service.dart b/lib/services/android_file_service.dart index 0a0bb2f12..7b8668315 100644 --- a/lib/services/android_file_service.dart +++ b/lib/services/android_file_service.dart @@ -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> getStorageVolumes() async { + static Future> getStorageVolumes() async { try { final result = await platform.invokeMethod('getStorageVolumes'); - return (result as List).cast(); + return (result as List).cast().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 getFreeSpace(StorageVolume volume) async { diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index 7565db251..949e3424a 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -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 storageVolumes = {}; - Map _installedAppNameMap = {}; + Set _packages = {}; + List _potentialAppDirs = []; AChangeNotifier appNameChangeNotifier = AChangeNotifier(); + Iterable get _launcherPackages => _packages.where((package) => package.categoryLauncher); + AndroidFileUtils._private(); Future 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 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 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 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; diff --git a/lib/widgets/debug/android_apps.dart b/lib/widgets/debug/android_apps.dart index e929e34eb..9854088c1 100644 --- a/lib/widgets/debug/android_apps.dart +++ b/lib/widgets/debug/android_apps.dart @@ -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 with AutomaticKeepAliveClientMixin { - Future _loader; + Future> _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 with Au children: [ Padding( padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: FutureBuilder( + child: FutureBuilder>( 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 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 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, ), ], diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index d2ba17492..b900b4b7c 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -136,7 +136,7 @@ class _OwnerPropState extends State { 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: [