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> <queries>
<intent> <intent>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent> </intent>
</queries> </queries>

View file

@ -13,6 +13,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe 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.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -29,7 +30,7 @@ import kotlin.math.roundToInt
class AppAdapterHandler(private val context: Context) : MethodCallHandler { class AppAdapterHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { 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) } "getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppIcon) }
"edit" -> { "edit" -> {
val title = call.argument<String>("title") val title = call.argument<String>("title")
@ -62,46 +63,51 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} }
} }
private fun getAppNames(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { private fun getPackages(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
val nameMap = HashMap<String, String>() val packages = HashMap<String, FieldMap>()
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 fun addPackageDetails(intent: Intent) {
// so we get their names in English as well as the current locale // apps tend to use their name in English when creating folders
val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) } // so we get their names in English as well as the current locale
val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) }
val pm = context.packageManager val pm = context.packageManager
for (resolveInfo in pm.queryIntentActivities(intent, 0)) { for (resolveInfo in pm.queryIntentActivities(intent, 0)) {
val ai = resolveInfo.activityInfo.applicationInfo val appInfo = resolveInfo.activityInfo.applicationInfo
val isSystemPackage = ai.flags and ApplicationInfo.FLAG_SYSTEM != 0 val packageName = appInfo.packageName
if (!isSystemPackage) { if (!packages.containsKey(packageName)) {
val packageName = ai.packageName val currentLabel = pm.getApplicationLabel(appInfo).toString()
val englishLabel: String? = appInfo.labelRes.takeIf { it != 0 }?.let { labelRes ->
val currentLabel = pm.getApplicationLabel(ai).toString() var englishLabel: String? = null
nameMap[currentLabel] = packageName try {
val resources = pm.getResourcesForApplication(appInfo)
val labelRes = ai.labelRes // `updateConfiguration` is deprecated but it seems to be the only way
if (labelRes != 0) { // to query resources from another app with a specific locale.
try { // The following methods do not work:
val resources = pm.getResourcesForApplication(ai) // - `resources.getConfiguration().setLocale(...)`
// `updateConfiguration` is deprecated but it seems to be the only way // - getting a package manager from a custom context with `context.createConfigurationContext(config)`
// to query resources from another app with a specific locale. @Suppress("DEPRECATION")
// The following methods do not work: resources.updateConfiguration(englishConfig, resources.displayMetrics)
// - `resources.getConfiguration().setLocale(...)` englishLabel = resources.getString(labelRes)
// - getting a package manager from a custom context with `context.createConfigurationContext(config)` } catch (e: PackageManager.NameNotFoundException) {
@Suppress("DEPRECATION") Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e)
resources.updateConfiguration(englishConfig, resources.displayMetrics) }
val englishLabel = resources.getString(labelRes) englishLabel
nameMap[englishLabel] = packageName
} catch (e: PackageManager.NameNotFoundException) {
Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e)
} }
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) { 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.ExifInterfaceHelper
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
import deckers.thibault.aves.metadata.Metadata 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.LogUtils
import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface 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.ThumbnailFetcher
import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher
import deckers.thibault.aves.model.ExifOrientationOp 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.ImageProvider.ImageOpCallback
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.model.provider.MediaStoreImageProvider 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.getSafeDateMillis
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
import deckers.thibault.aves.metadata.XMP.isPanorama 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.FileImageProvider
import deckers.thibault.aves.model.provider.ImageProvider import deckers.thibault.aves.model.provider.ImageProvider
import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.BitmapUtils

View file

@ -6,7 +6,7 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import deckers.thibault.aves.model.AvesEntry 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.ImageProvider.ImageOpCallback
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils

View file

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

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.model package deckers.thibault.aves.model
import android.net.Uri import android.net.Uri
import deckers.thibault.aves.model.provider.FieldMap import deckers.thibault.aves.model.FieldMap
class AvesEntry(map: FieldMap) { class AvesEntry(map: FieldMap) {
val uri: Uri = Uri.parse(map["uri"] as String) // content or file URI 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.getSafeDateMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong 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.MimeTypes
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import org.beyka.tiffbitmapfactory.TiffBitmapFactory 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.decoder.TiffImage
import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.ExifOrientationOp import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
@ -348,5 +349,3 @@ abstract class ImageProvider {
private val LOG_TAG = LogUtils.createTag(ImageProvider::class.java) 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 android.util.Log
import com.commonsware.cwac.document.DocumentFileCompat import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes

View file

@ -1,6 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -8,12 +9,18 @@ import 'package:flutter/services.dart';
class AndroidAppService { class AndroidAppService {
static const platform = MethodChannel('deckers.thibault/aves/app'); static const platform = MethodChannel('deckers.thibault/aves/app');
static Future<Map> getAppNames() async { static Future<Set<Package>> getPackages() async {
try { try {
final result = await platform.invokeMethod('getAppNames'); final result = await platform.invokeMethod('getPackages');
return result as Map; 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) { } 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 {}; return {};
} }

View file

@ -9,14 +9,14 @@ class AndroidFileService {
static const platform = MethodChannel('deckers.thibault/aves/storage'); static const platform = MethodChannel('deckers.thibault/aves/storage');
static final StreamsChannel storageAccessChannel = StreamsChannel('deckers.thibault/aves/storageaccessstream'); static final StreamsChannel storageAccessChannel = StreamsChannel('deckers.thibault/aves/storageaccessstream');
static Future<List<Map>> getStorageVolumes() async { static Future<Set<StorageVolume>> getStorageVolumes() async {
try { try {
final result = await platform.invokeMethod('getStorageVolumes'); 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) { } on PlatformException catch (e) {
debugPrint('getStorageVolumes failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); debugPrint('getStorageVolumes failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
} }
return []; return {};
} }
static Future<int> getFreeSpace(StorageVolume volume) async { 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_app_service.dart';
import 'package:aves/services/android_file_service.dart'; import 'package:aves/services/android_file_service.dart';
import 'package:aves/utils/change_notifier.dart'; import 'package:aves/utils/change_notifier.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
@ -8,14 +9,17 @@ final AndroidFileUtils androidFileUtils = AndroidFileUtils._private();
class AndroidFileUtils { class AndroidFileUtils {
String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath; String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath;
Set<StorageVolume> storageVolumes = {}; Set<StorageVolume> storageVolumes = {};
Map _installedAppNameMap = {}; Set<Package> _packages = {};
List<String> _potentialAppDirs = [];
AChangeNotifier appNameChangeNotifier = AChangeNotifier(); AChangeNotifier appNameChangeNotifier = AChangeNotifier();
Iterable<Package> get _launcherPackages => _packages.where((package) => package.categoryLauncher);
AndroidFileUtils._private(); AndroidFileUtils._private();
Future<void> init() async { 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' // path_provider getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files'
primaryStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path; primaryStorage = storageVolumes.firstWhere((volume) => volume.isPrimary).path;
dcimPath = join(primaryStorage, 'DCIM'); dcimPath = join(primaryStorage, 'DCIM');
@ -25,8 +29,8 @@ class AndroidFileUtils {
} }
Future<void> initAppNames() async { Future<void> initAppNames() async {
_installedAppNameMap = await AndroidAppService.getAppNames() _packages = await AndroidAppService.getPackages();
..addAll({'KakaoTalkDownload': 'com.kakao.talk'}); _potentialAppDirs = _launcherPackages.expand((package) => package.potentialDirs).toList();
appNameChangeNotifier.notifyListeners(); appNameChangeNotifier.notifyListeners();
} }
@ -42,28 +46,67 @@ class AndroidFileUtils {
bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false; bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false;
AlbumType getAlbumType(String albumDirectory) { AlbumType getAlbumType(String albumPath) {
if (albumDirectory != null) { if (albumPath != null) {
if (isCameraPath(albumDirectory)) return AlbumType.camera; if (isCameraPath(albumPath)) return AlbumType.camera;
if (isDownloadPath(albumDirectory)) return AlbumType.download; if (isDownloadPath(albumPath)) return AlbumType.download;
if (isScreenRecordingsPath(albumDirectory)) return AlbumType.screenRecordings; if (isScreenRecordingsPath(albumPath)) return AlbumType.screenRecordings;
if (isScreenshotsPath(albumDirectory)) return AlbumType.screenshots; if (isScreenshotsPath(albumPath)) return AlbumType.screenshots;
final parts = albumDirectory.split(separator); final dir = albumPath.split(separator).last;
if (albumDirectory.startsWith(primaryStorage) && _isInstalledAppName(parts.last)) return AlbumType.app; if (albumPath.startsWith(primaryStorage) && _potentialAppDirs.contains(dir)) return AlbumType.app;
} }
return AlbumType.regular; 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 getCurrentAppName(String packageName) {
final package = _packages.firstWhere((package) => package.packageName == packageName, orElse: () => null);
String getAppName(String packageName) => _installedAppNameMap.entries.firstWhere((kv) => kv.value == packageName, orElse: () => null)?.key; return package?.currentLabel;
}
} }
enum AlbumType { regular, app, camera, download, screenRecordings, screenshots } 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 { class StorageVolume {
final String description, path, state; final String description, path, state;
final bool isEmulated, isPrimary, isRemovable; final bool isEmulated, isPrimary, isRemovable;

View file

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

View file

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