From 4beef9a3605f6a1564e02a45cff20b7064489f4f Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Mon, 12 Jan 2026 08:58:43 +0800 Subject: [PATCH] Add option to include system apps in installed apps retrieval (#549) --- .../proxy/plugin/InstalledAppsPlugin.kt | 36 ++++-- lib/native/installed_apps.dart | 14 ++- lib/network/bin/configuration.dart | 2 +- lib/ui/desktop/desktop.dart | 2 + lib/ui/mobile/setting/app_filter.dart | 116 ++++++++++++------ lib/utils/task.dart | 51 ++++++++ 6 files changed, 168 insertions(+), 53 deletions(-) create mode 100644 lib/utils/task.dart diff --git a/android/app/src/main/kotlin/com/network/proxy/plugin/InstalledAppsPlugin.kt b/android/app/src/main/kotlin/com/network/proxy/plugin/InstalledAppsPlugin.kt index 2b3373f..fd1bffc 100644 --- a/android/app/src/main/kotlin/com/network/proxy/plugin/InstalledAppsPlugin.kt +++ b/android/app/src/main/kotlin/com/network/proxy/plugin/InstalledAppsPlugin.kt @@ -28,7 +28,16 @@ class InstalledAppsPlugin : AndroidFlutterPlugin() { "getInstalledApps" -> { val withIcon = call.argument("withIcon") ?: false val packageNamePrefix = call.argument("packageNamePrefix") ?: "" - result.success(getInstalledApps(withIcon, packageNamePrefix)) + val includeSystemApps = call.argument("includeSystemApps") ?: false + Thread { + result.success( + getInstalledApps( + withIcon, + packageNamePrefix, + includeSystemApps + ) + ) + }.start() } "getAppInfo" -> { @@ -48,27 +57,33 @@ class InstalledAppsPlugin : AndroidFlutterPlugin() { } } + private fun isSystemApp(applicationInfo: ApplicationInfo?): Boolean { + if (applicationInfo == null) return false + return (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0 + } + private fun getInstalledApps( withIcon: Boolean, - packageNamePrefix: String + packageNamePrefix: String, + includeSystemApps: Boolean ): List { val packageManager = activity.packageManager var installedApps = packageManager.getInstalledApplications(0) - installedApps = - installedApps.filter { app -> - (app.flags and ApplicationInfo.FLAG_SYSTEM) <= 0 - || (app.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 - || packageManager.getLaunchIntentForPackage(app.packageName) != null - } - if (packageNamePrefix.isNotEmpty()) + if (!includeSystemApps) { + installedApps = + installedApps.filter { app -> !isSystemApp(app) } + } + + if (packageNamePrefix.isNotEmpty()) { installedApps = installedApps.filter { app -> app.packageName.startsWith( packageNamePrefix.lowercase(Locale.ENGLISH) ) } + } - val threadPoolExecutor = Executors.newFixedThreadPool(6) + val threadPoolExecutor = Executors.newFixedThreadPool(4) installedApps.map { app -> val task: Callable = Callable { ProcessInfo.create(packageManager, app, withIcon) @@ -84,4 +99,3 @@ class InstalledAppsPlugin : AndroidFlutterPlugin() { } } - diff --git a/lib/native/installed_apps.dart b/lib/native/installed_apps.dart index 13d0d94..714c351 100644 --- a/lib/native/installed_apps.dart +++ b/lib/native/installed_apps.dart @@ -3,10 +3,16 @@ import 'package:flutter/services.dart'; class InstalledApps { static const MethodChannel _methodChannel = MethodChannel('com.proxy/installedApps'); - static Future> getInstalledApps(bool withIcon, {String? packageNamePrefix}) { - return _methodChannel - .invokeListMethod('getInstalledApps', {"withIcon": withIcon, "packageNamePrefix": packageNamePrefix}).then( - (value) => value?.map((e) => AppInfo.formJson(e)).toList() ?? []); + static Future> getInstalledApps( + bool withIcon, { + String? packageNamePrefix, + bool includeSystemApps = false, + }) { + return _methodChannel.invokeListMethod('getInstalledApps', { + "withIcon": withIcon, + "packageNamePrefix": packageNamePrefix, + "includeSystemApps": includeSystemApps, + }).then((value) => value?.map((e) => AppInfo.formJson(e)).toList() ?? []); } static Future getAppInfo(String packageName) async { diff --git a/lib/network/bin/configuration.dart b/lib/network/bin/configuration.dart index 6e15efb..9a22a4e 100644 --- a/lib/network/bin/configuration.dart +++ b/lib/network/bin/configuration.dart @@ -29,7 +29,7 @@ class Configuration { int port = 9099; //是否启用https抓包 - bool enableSsl = false; + bool enableSsl = Platforms.isMobile(); //是否设置系统代理 bool enableSystemProxy = true; diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart index 0eaedf6..b2ed50c 100644 --- a/lib/ui/desktop/desktop.dart +++ b/lib/ui/desktop/desktop.dart @@ -179,6 +179,8 @@ class _DesktopHomePagePageState extends State implements EventL '2. 增加请求解密,可配置AES自动解密消息体;\n' '3. HTTP Header 展示增加文本和表格切换;\n' '4. 增加 Request Param 列表展示;\n' + '5. 应用过滤列表增加是否显示系统应用;\n' + '6. 更新JSON深色主题色,以提高可见度和美观度;\n' '8. bug修复和改进;\n' : 'Note: HTTPS capture is disabled by default — please install the certificate before enabling HTTPS capture.\n\n' '1. Added WebSocket request testing in the Toolbox.\n' diff --git a/lib/ui/mobile/setting/app_filter.dart b/lib/ui/mobile/setting/app_filter.dart index 5313cb7..ec78f57 100644 --- a/lib/ui/mobile/setting/app_filter.dart +++ b/lib/ui/mobile/setting/app_filter.dart @@ -16,12 +16,14 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:proxypin/l10n/app_localizations.dart'; import 'package:proxypin/native/installed_apps.dart'; import 'package:proxypin/native/vpn.dart'; import 'package:proxypin/network/bin/configuration.dart'; import 'package:proxypin/network/bin/server.dart'; import 'package:proxypin/ui/component/widgets.dart'; +import 'package:proxypin/utils/task.dart'; ///应用白名单 目前只支持安卓 ios没办法获取安装的列表 ///@author wang @@ -317,10 +319,41 @@ class InstalledAppsWidget extends StatefulWidget { } class _InstalledAppsWidgetState extends State { - static Future> apps = InstalledApps.getInstalledApps(true); + static List? apps; + static bool includeSystemApps = false; + + RxBool loading = false.obs; String? keyword; + @override + void initState() { + super.initState(); + DelayedTask().cancel("InstalledAppsWidget_release"); + if (apps != null) { + return; + } + refreshApps(); + } + + @override + void dispose() { + DelayedTask().debounce("InstalledAppsWidget_release", const Duration(seconds: 10), () { + apps = null; + includeSystemApps = false; + }); + super.dispose(); + } + + void refreshApps() async { + try { + loading.value = true; + apps = await InstalledApps.getInstalledApps(true, includeSystemApps: includeSystemApps); + } finally { + loading.value = false; + } + } + @override Widget build(BuildContext context) { bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); @@ -331,6 +364,18 @@ class _InstalledAppsWidgetState extends State { decoration: InputDecoration( hintText: isCN ? "请输入应用名或包名" : "Please enter the application or package name", border: InputBorder.none, + hintStyle: TextStyle(color: Colors.grey.shade500), + suffixIcon: IconButton( + color: includeSystemApps ? Theme.of(context).colorScheme.primary : null, + icon: const Icon(Icons.visibility_outlined), + tooltip: isCN ? "显示系统应用" : "Show system apps", + onPressed: () { + setState(() { + includeSystemApps = !includeSystemApps; + }); + refreshApps(); + }, + ), ), onChanged: (String value) { keyword = value.toLowerCase(); @@ -340,45 +385,42 @@ class _InstalledAppsWidgetState extends State { ), body: RefreshIndicator( onRefresh: () async { - apps = InstalledApps.getInstalledApps(true); - await apps; - setState(() {}); + refreshApps(); }, - child: FutureBuilder( - future: apps, - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasData) { - List appInfoList = snapshot.data!; - appInfoList = appInfoList.toSet().difference(widget.addedList.toSet()).toList(); - if (keyword != null && keyword!.trim().isNotEmpty) { - appInfoList = appInfoList - .where((element) => - element.name!.toLowerCase().contains(keyword!) || - element.packageName!.toLowerCase().contains(keyword!)) - .toList(); - } - - return ListView.builder( - itemCount: appInfoList.length, - itemBuilder: (BuildContext context, int index) { - AppInfo appInfo = appInfoList[index]; - return ListTile( - leading: Image.memory(appInfo.icon ?? Uint8List(0)), - title: Text(appInfo.name ?? ""), - subtitle: Text(appInfo.packageName ?? ""), - onTap: () async { - Navigator.of(context).pop(appInfo.packageName); - }, - ); - }); - } else { - return const Center( + child: Obx(() => loading.value + ? const Center( child: CircularProgressIndicator(), - ); - } - }, - ), + ) + : buildAppListView()), ), ); } + + ListView buildAppListView() { + if (apps == null) { + return ListView(); + } + List appInfoList = apps!; + appInfoList = appInfoList.toSet().difference(widget.addedList.toSet()).toList(); + if (keyword != null && keyword!.trim().isNotEmpty) { + appInfoList = appInfoList + .where((element) => + element.name!.toLowerCase().contains(keyword!) || element.packageName!.toLowerCase().contains(keyword!)) + .toList(); + } + + return ListView.builder( + itemCount: appInfoList.length, + itemBuilder: (BuildContext context, int index) { + AppInfo appInfo = appInfoList[index]; + return ListTile( + leading: Image.memory(appInfo.icon ?? Uint8List(0)), + title: Text(appInfo.name ?? ""), + subtitle: Text(appInfo.packageName ?? ""), + onTap: () async { + Navigator.of(context).pop(appInfo.packageName); + }, + ); + }); + } } diff --git a/lib/utils/task.dart b/lib/utils/task.dart new file mode 100644 index 0000000..85e976c --- /dev/null +++ b/lib/utils/task.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +/// 延时任务工具类 +class DelayedTask { + // 私有构造函数,实现单例 + DelayedTask._internal(); + + static final DelayedTask _instance = DelayedTask._internal(); + + factory DelayedTask() => _instance; + + // 维护一个任务池,支持同时管理多个不同的延时任务 + final Map _taskPool = {}; + + /// 执行防抖任务 (Debounce) + /// 如果在 [duration] 时间内再次调用相同 [tag] 的任务,前一个任务会被自动取消 + void debounce( + String tag, + Duration duration, + void Function() action, + ) { + // 1. 如果旧任务还在运行,直接取消 + _taskPool[tag]?.cancel(); + + // 2. 开启新任务 + _taskPool[tag] = Timer(duration, () { + action(); + _taskPool.remove(tag); // 执行完毕后移除 + }); + } + + /// 延迟 [duration] 后执行一次,返回可手动取消的 Timer + /// 适用于不需要防抖,但需要精准手动控制取消的场景 + Timer delay(Duration duration, void Function() action) { + return Timer(duration, action); + } + + /// 取消特定标签的任务 + void cancel(String tag) { + if (_taskPool.containsKey(tag)) { + _taskPool[tag]?.cancel(); + _taskPool.remove(tag); + } + } + + /// 取消所有正在运行的任务 (通常在 dispose 时调用) + void cancelAll() { + _taskPool.forEach((tag, timer) => timer.cancel()); + _taskPool.clear(); + } +}