Add option to include system apps in installed apps retrieval (#549)

This commit is contained in:
wanghongenpin
2026-01-12 08:58:43 +08:00
parent 7261b46559
commit 4beef9a360
6 changed files with 168 additions and 53 deletions

View File

@@ -28,7 +28,16 @@ class InstalledAppsPlugin : AndroidFlutterPlugin() {
"getInstalledApps" -> {
val withIcon = call.argument<Boolean>("withIcon") ?: false
val packageNamePrefix = call.argument<String>("packageNamePrefix") ?: ""
result.success(getInstalledApps(withIcon, packageNamePrefix))
val includeSystemApps = call.argument<Boolean>("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<ProcessInfo> {
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<ProcessInfo> = Callable {
ProcessInfo.create(packageManager, app, withIcon)
@@ -84,4 +99,3 @@ class InstalledAppsPlugin : AndroidFlutterPlugin() {
}
}

View File

@@ -3,10 +3,16 @@ import 'package:flutter/services.dart';
class InstalledApps {
static const MethodChannel _methodChannel = MethodChannel('com.proxy/installedApps');
static Future<List<AppInfo>> getInstalledApps(bool withIcon, {String? packageNamePrefix}) {
return _methodChannel
.invokeListMethod<Map>('getInstalledApps', {"withIcon": withIcon, "packageNamePrefix": packageNamePrefix}).then(
(value) => value?.map((e) => AppInfo.formJson(e)).toList() ?? []);
static Future<List<AppInfo>> getInstalledApps(
bool withIcon, {
String? packageNamePrefix,
bool includeSystemApps = false,
}) {
return _methodChannel.invokeListMethod<Map>('getInstalledApps', {
"withIcon": withIcon,
"packageNamePrefix": packageNamePrefix,
"includeSystemApps": includeSystemApps,
}).then((value) => value?.map((e) => AppInfo.formJson(e)).toList() ?? []);
}
static Future<AppInfo> getAppInfo(String packageName) async {

View File

@@ -29,7 +29,7 @@ class Configuration {
int port = 9099;
//是否启用https抓包
bool enableSsl = false;
bool enableSsl = Platforms.isMobile();
//是否设置系统代理
bool enableSystemProxy = true;

View File

@@ -179,6 +179,8 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> 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'

View File

@@ -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<InstalledAppsWidget> {
static Future<List<AppInfo>> apps = InstalledApps.getInstalledApps(true);
static List<AppInfo>? 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<InstalledAppsWidget> {
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<InstalledAppsWidget> {
),
body: RefreshIndicator(
onRefresh: () async {
apps = InstalledApps.getInstalledApps(true);
await apps;
setState(() {});
refreshApps();
},
child: FutureBuilder(
future: apps,
builder: (BuildContext context, AsyncSnapshot<List<AppInfo>> snapshot) {
if (snapshot.hasData) {
List<AppInfo> 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<AppInfo> 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);
},
);
});
}
}

51
lib/utils/task.dart Normal file
View File

@@ -0,0 +1,51 @@
import 'dart:async';
/// 延时任务工具类
class DelayedTask {
// 私有构造函数,实现单例
DelayedTask._internal();
static final DelayedTask _instance = DelayedTask._internal();
factory DelayedTask() => _instance;
// 维护一个任务池,支持同时管理多个不同的延时任务
final Map<String, Timer> _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();
}
}