Add android per-app proxy

This commit is contained in:
problematicconsumer
2023-09-13 23:19:16 +03:30
parent f1b0f8ee4b
commit ea6f8b5fad
16 changed files with 587 additions and 37 deletions

View File

@@ -1,10 +1,21 @@
package com.hiddify.hiddify
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.VectorDrawable
import android.net.Uri
import android.os.Build
import android.util.Base64
import androidx.annotation.NonNull
import androidx.core.graphics.drawable.toBitmap
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.hiddify.hiddify.Application.Companion.packageManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@@ -12,6 +23,10 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import io.flutter.plugin.common.StandardMethodCodec
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware,
PluginRegistry.ActivityResultListener {
@@ -23,10 +38,13 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
const val channelName = "com.hiddify.app/platform.settings"
const val REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = 44
val gson = Gson()
enum class Trigger(val method: String) {
IsIgnoringBatteryOptimizations("is_ignoring_battery_optimizations"),
RequestIgnoreBatteryOptimizations("request_ignore_battery_optimizations"),
GetInstalledPackages("get_installed_packages"),
GetPackagesIcon("get_package_icon"),
}
}
@@ -71,6 +89,12 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
return false
}
data class AppItem(
@SerializedName("package-name") val packageName: String,
@SerializedName("name") val name: String,
@SerializedName("is-system-app") val isSystemApp: Boolean
)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
Trigger.IsIgnoringBatteryOptimizations.method -> {
@@ -97,6 +121,69 @@ class PlatformSettingsHandler : FlutterPlugin, MethodChannel.MethodCallHandler,
activity?.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
}
Trigger.GetInstalledPackages.method -> {
GlobalScope.launch {
result.runCatching {
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.GET_PERMISSIONS or PackageManager.MATCH_UNINSTALLED_PACKAGES
} else {
@Suppress("DEPRECATION")
PackageManager.GET_PERMISSIONS or PackageManager.GET_UNINSTALLED_PACKAGES
}
val installedPackages =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getInstalledPackages(
PackageManager.PackageInfoFlags.of(
flag.toLong()
)
)
} else {
@Suppress("DEPRECATION")
packageManager.getInstalledPackages(flag)
}
val list = mutableListOf<AppItem>()
installedPackages.forEach {
if (it.packageName != Application.application.packageName &&
(it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android")
) {
list.add(
AppItem(
it.packageName,
it.applicationInfo.loadLabel(packageManager).toString(),
it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
)
)
}
}
list.sortBy { it.name }
success(gson.toJson(list))
}
}
}
Trigger.GetPackagesIcon.method -> {
result.runCatching {
val args = call.arguments as Map<*, *>
val packageName =
args["packageName"] as String? ?: return error("provide packageName")
val drawable = packageManager.getApplicationIcon(packageName)
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
val base64: String =
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
success(base64)
}
}
else -> result.notImplemented()
}
}

View File

@@ -1,41 +1,62 @@
package com.hiddify.hiddify
import android.content.Context
import android.util.Base64
import com.hiddify.hiddify.bg.ProxyService
import com.hiddify.hiddify.bg.VPNService
import com.hiddify.hiddify.constant.PerAppProxyMode
import com.hiddify.hiddify.constant.ServiceMode
import com.hiddify.hiddify.constant.SettingsKey
import org.json.JSONObject
import java.io.ByteArrayInputStream
import java.io.File
import java.io.ObjectInputStream
object Settings {
const val PER_APP_PROXY_DISABLED = 0
const val PER_APP_PROXY_EXCLUDE = 1
const val PER_APP_PROXY_INCLUDE = 2
private val preferences by lazy {
val context = Application.application.applicationContext
context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
}
var perAppProxyEnabled = preferences.getBoolean(SettingsKey.PER_APP_PROXY_ENABLED, false)
var perAppProxyMode = preferences.getInt(SettingsKey.PER_APP_PROXY_MODE, PER_APP_PROXY_EXCLUDE)
var perAppProxyList = preferences.getStringSet(SettingsKey.PER_APP_PROXY_LIST, emptySet())!!
var perAppProxyUpdateOnChange =
preferences.getInt(SettingsKey.PER_APP_PROXY_UPDATE_ON_CHANGE, PER_APP_PROXY_DISABLED)
private const val LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"
var perAppProxyMode: String
get() = preferences.getString(SettingsKey.PER_APP_PROXY_MODE, PerAppProxyMode.OFF)!!
set(value) = preferences.edit().putString(SettingsKey.PER_APP_PROXY_MODE, value).apply()
val perAppProxyEnabled: Boolean
get() = perAppProxyMode != PerAppProxyMode.OFF
val perAppProxyList: List<String>
get() {
val stringValue = if (perAppProxyMode == PerAppProxyMode.INCLUDE) {
preferences.getString(SettingsKey.PER_APP_PROXY_INCLUDE_LIST, "")!!;
} else {
preferences.getString(SettingsKey.PER_APP_PROXY_EXCLUDE_LIST, "")!!;
}
if (!stringValue.startsWith(LIST_IDENTIFIER)) {
return emptyList()
}
return decodeListString(stringValue.substring(LIST_IDENTIFIER.length))
}
private fun decodeListString(listString: String): List<String> {
val stream = ObjectInputStream(ByteArrayInputStream(Base64.decode(listString, 0)))
return stream.readObject() as List<String>
}
var activeConfigPath: String
get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "") ?: ""
get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "")!!
set(value) = preferences.edit().putString(SettingsKey.ACTIVE_CONFIG_PATH, value).apply()
var serviceMode: String
get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.NORMAL)
?: ServiceMode.NORMAL
get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.NORMAL)!!
set(value) = preferences.edit().putString(SettingsKey.SERVICE_MODE, value).apply()
var configOptions: String
get() = preferences.getString(SettingsKey.CONFIG_OPTIONS, "") ?: ""
get() = preferences.getString(SettingsKey.CONFIG_OPTIONS, "")!!
set(value) = preferences.edit().putString(SettingsKey.CONFIG_OPTIONS, value).apply()
var debugMode: Boolean
@@ -47,11 +68,13 @@ object Settings {
var disableMemoryLimit: Boolean
get() = preferences.getBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, false)
set(value) = preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply()
set(value) =
preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply()
var systemProxyEnabled: Boolean
get() = preferences.getBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, true)
set(value) = preferences.edit().putBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, value).apply()
set(value) =
preferences.edit().putBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, value).apply()
var startedByUser: Boolean
get() = preferences.getBoolean(SettingsKey.STARTED_BY_USER, false)

View File

@@ -16,22 +16,22 @@ class AppChangeReceiver : BroadcastReceiver() {
}
private fun checkUpdate(context: Context, intent: Intent) {
if (!Settings.perAppProxyEnabled) {
return
}
val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange
if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) {
return
}
val packageName = intent.dataString?.substringAfter("package:")
if (packageName.isNullOrBlank()) {
return
}
if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE)) {
Settings.perAppProxyList = Settings.perAppProxyList + packageName
} else {
Settings.perAppProxyList = Settings.perAppProxyList - packageName
}
// if (!Settings.perAppProxyEnabled) {
// return
// }
// val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange
// if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) {
// return
// }
// val packageName = intent.dataString?.substringAfter("package:")
// if (packageName.isNullOrBlank()) {
// return
// }
// if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE)) {
// Settings.perAppProxyList = Settings.perAppProxyList + packageName
// } else {
// Settings.perAppProxyList = Settings.perAppProxyList - packageName
// }
}
}

View File

@@ -6,6 +6,7 @@ import android.content.pm.PackageManager.NameNotFoundException
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Build
import com.hiddify.hiddify.constant.PerAppProxyMode
import io.nekohasekai.libbox.TunOptions
class VPNService : VpnService(), PlatformInterfaceWrapper {
@@ -87,7 +88,7 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
if (Settings.perAppProxyEnabled) {
val appList = Settings.perAppProxyList
if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) {
if (Settings.perAppProxyMode == PerAppProxyMode.INCLUDE) {
appList.forEach {
try {
builder.addAllowedApplication(it)

View File

@@ -0,0 +1,7 @@
package com.hiddify.hiddify.constant
object PerAppProxyMode {
const val OFF = "off"
const val INCLUDE = "include"
const val EXCLUDE = "exclude"
}

View File

@@ -8,10 +8,9 @@ object SettingsKey {
const val CONFIG_OPTIONS = "config_options_json"
const val PER_APP_PROXY_ENABLED = "per_app_proxy_enabled"
const val PER_APP_PROXY_MODE = "per_app_proxy_mode"
const val PER_APP_PROXY_LIST = "per_app_proxy_list"
const val PER_APP_PROXY_UPDATE_ON_CHANGE = "per_app_proxy_update_on_change"
const val PER_APP_PROXY_MODE = "${KEY_PREFIX}per_app_proxy_mode"
const val PER_APP_PROXY_INCLUDE_LIST = "${KEY_PREFIX}per_app_proxy_include_list"
const val PER_APP_PROXY_EXCLUDE_LIST = "${KEY_PREFIX}per_app_proxy_exclude_list"
const val DEBUG_MODE = "${KEY_PREFIX}debug_mode"
const val ENABLE_TUN = "${KEY_PREFIX}enable-tun"

View File

@@ -117,6 +117,20 @@
"debugMode": "Debug Mode",
"debugModeMsg": "Restart the app for applying this change"
},
"network": {
"perAppProxyPageTitle": "Per-app Proxy",
"perAppProxyModes": {
"off": "All",
"offMsg": "Proxy all apps",
"include": "Proxy",
"includeMsg": "Proxy only selected apps",
"exclude": "Bypass",
"excludeMsg": "Do not proxy selected apps"
},
"showSystemApps": "Show system apps",
"hideSystemApps": "Hide system apps",
"clearSelection": "Clear selection"
},
"config": {
"section": {
"route": "Route Options",

View File

@@ -117,6 +117,20 @@
"debugMode": "دیباگ مود",
"debugModeMsg": "برای اعمال این تغییر اپ را ری‌استارت کنید"
},
"network": {
"perAppProxyPageTitle": "پراکسی برنامه‌ها",
"perAppProxyModes": {
"off": "همه",
"offMsg": "همه برنامه‌ها پراکسی میشوند",
"include": "پراکسی",
"includeMsg": "تنها برنامه‌های انتخاب شده پراکسی میشوند",
"exclude": "بایپس",
"excludeMsg": "همه بجز برنامه‌های انتخاب شده پراکسی میشوند"
},
"showSystemApps": "نمایش برنامه‌های سیستمی",
"hideSystemApps": "مخفی کردن برنامه‌های سیستمی",
"clearSelection": "حذف انتخاب‌ها"
},
"config": {
"section": {
"route": "تنظیمات مسیریاب",

View File

@@ -1,6 +1,7 @@
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/environment.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hiddify/utils/pref_notifier.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -37,6 +38,54 @@ class DebugModeNotifier extends _$DebugModeNotifier {
}
}
@Riverpod(keepAlive: true)
class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier {
late final _pref = Pref(
ref.watch(sharedPreferencesProvider),
"per_app_proxy_mode",
PerAppProxyMode.off,
mapFrom: PerAppProxyMode.values.byName,
mapTo: (value) => value.name,
);
@override
PerAppProxyMode build() => _pref.getValue();
Future<void> update(PerAppProxyMode value) {
state = value;
return _pref.update(value);
}
}
@Riverpod(keepAlive: true)
class PerAppProxyList extends _$PerAppProxyList {
late final _include = Pref(
ref.watch(sharedPreferencesProvider),
"per_app_proxy_include_list",
<String>[],
);
late final _exclude = Pref(
ref.watch(sharedPreferencesProvider),
"per_app_proxy_exclude_list",
<String>[],
);
@override
List<String> build() =>
ref.watch(perAppProxyModeNotifierProvider) == PerAppProxyMode.include
? _include.getValue()
: _exclude.getValue();
Future<void> update(List<String> value) {
state = value;
if (ref.read(perAppProxyModeNotifierProvider) == PerAppProxyMode.include) {
return _include.update(value);
}
return _exclude.update(value);
}
}
@riverpod
class MarkNewProfileActive extends _$MarkNewProfileActive {
late final _pref = Pref(

View File

@@ -22,6 +22,7 @@ part 'mobile_routes.g.dart';
path: SettingsRoute.path,
routes: [
TypedGoRoute<ConfigOptionsRoute>(path: ConfigOptionsRoute.path),
TypedGoRoute<PerAppProxyRoute>(path: PerAppProxyRoute.path),
],
),
TypedGoRoute<AboutRoute>(path: AboutRoute.path),
@@ -84,6 +85,21 @@ class ConfigOptionsRoute extends GoRouteData {
}
}
class PerAppProxyRoute extends GoRouteData {
const PerAppProxyRoute();
static const path = 'per-app-proxy';
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return const MaterialPage(
fullscreenDialog: true,
child: PerAppProxyPage(),
);
}
}
class AboutRoute extends GoRouteData {
const AboutRoute();
static const path = 'about';

View File

@@ -0,0 +1,24 @@
import 'package:hiddify/core/prefs/locale_prefs.dart';
enum PerAppProxyMode {
off,
include,
exclude;
bool get enabled => this != off;
({String title, String message}) present(TranslationsEn t) => switch (this) {
off => (
title: t.settings.network.perAppProxyModes.off,
message: t.settings.network.perAppProxyModes.offMsg,
),
include => (
title: t.settings.network.perAppProxyModes.include,
message: t.settings.network.perAppProxyModes.includeMsg,
),
exclude => (
title: t.settings.network.perAppProxyModes.exclude,
message: t.settings.network.perAppProxyModes.excludeMsg,
),
};
}

View File

@@ -2,4 +2,5 @@ export 'config_options.dart';
export 'core_status.dart';
export 'outbounds.dart';
export 'proxy_type.dart';
export 'rules.dart';
export 'singbox_facade.dart';

View File

@@ -0,0 +1,226 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/general_prefs.dart';
import 'package:hiddify/domain/singbox/rules.dart';
import 'package:hiddify/services/platform_settings.dart';
import 'package:hiddify/services/service_providers.dart';
import 'package:hiddify/utils/riverpod_utils.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:loggy/loggy.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sliver_tools/sliver_tools.dart';
part 'per_app_proxy_page.g.dart';
final _logger = Loggy<AppLogger>("PerAppProxySettings");
@riverpod
Future<List<InstalledPackageInfo>> installedPackagesInfo(
InstalledPackagesInfoRef ref,
) async {
return ref
.watch(platformSettingsProvider)
.getInstalledPackages()
.getOrElse((l) {
_logger.warning("error getting installed packages: $l");
throw l;
}).run();
}
@riverpod
Future<ImageProvider> packageIcon(
PackageIconRef ref,
String packageName,
) async {
ref.disposeDelay(const Duration(seconds: 10));
final bytes = await ref
.watch(platformSettingsProvider)
.getPackageIcon(packageName)
.getOrElse((l) {
_logger.warning("error getting package icon: $l");
throw l;
}).run();
return MemoryImage(bytes);
}
class PerAppProxyPage extends HookConsumerWidget with PresLogger {
const PerAppProxyPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final localizations = MaterialLocalizations.of(context);
final asyncPackages = ref.watch(installedPackagesInfoProvider);
final perAppProxyMode = ref.watch(perAppProxyModeNotifierProvider);
final perAppProxyList = ref.watch(perAppProxyListProvider);
final showSystemApps = useState(true);
final isSearching = useState(false);
final searchQuery = useState("");
final filteredPackages = useMemoized(
() {
if (showSystemApps.value && searchQuery.value.isBlank) {
return asyncPackages;
}
return asyncPackages.whenData(
(value) {
Iterable<InstalledPackageInfo> result = value;
if (!showSystemApps.value) {
result = result.filter((e) => !e.isSystemApp);
}
if (!searchQuery.value.isBlank) {
result = result.filter(
(e) => e.name
.toLowerCase()
.contains(searchQuery.value.toLowerCase()),
);
}
return result.toList();
},
);
},
[asyncPackages, showSystemApps.value, searchQuery.value],
);
return Scaffold(
appBar: isSearching.value
? AppBar(
title: TextFormField(
onChanged: (value) => searchQuery.value = value,
autofocus: true,
decoration: InputDecoration.collapsed(
hintText: "${localizations.searchFieldLabel}...",
),
),
leading: IconButton(
onPressed: () {
searchQuery.value = "";
isSearching.value = false;
},
icon: const Icon(Icons.close),
tooltip: localizations.cancelButtonLabel,
),
)
: AppBar(
title: Text(t.settings.network.perAppProxyPageTitle),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => isSearching.value = true,
tooltip: localizations.searchFieldLabel,
),
PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
child: Text(
showSystemApps.value
? t.settings.network.hideSystemApps
: t.settings.network.showSystemApps,
),
onTap: () =>
showSystemApps.value = !showSystemApps.value,
),
PopupMenuItem(
child: Text(t.settings.network.clearSelection),
onTap: () => ref
.read(perAppProxyListProvider.notifier)
.update([]),
),
];
},
),
],
),
body: CustomScrollView(
slivers: [
SliverPinnedHeader(
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Column(
children: [
...PerAppProxyMode.values.map(
(e) => RadioListTile<PerAppProxyMode>(
title: Text(e.present(t).message),
dense: true,
value: e,
groupValue: perAppProxyMode,
onChanged: (value) async {
await ref
.read(perAppProxyModeNotifierProvider.notifier)
.update(e);
if (e == PerAppProxyMode.off && context.mounted) {
context.pop();
}
},
),
),
const Divider(height: 1),
],
),
),
),
switch (filteredPackages) {
AsyncData(value: final packages) => SliverList.builder(
itemBuilder: (context, index) {
final package = packages[index];
final selected =
perAppProxyList.contains(package.packageName);
return CheckboxListTile(
title: Text(
package.name,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(package.packageName),
value: selected,
onChanged: (value) async {
final List<String> newSelection;
if (selected) {
newSelection = perAppProxyList
.exceptElement(package.packageName)
.toList();
} else {
newSelection = [
...perAppProxyList,
package.packageName,
];
}
await ref
.read(perAppProxyListProvider.notifier)
.update(newSelection);
},
secondary: SizedBox(
width: 48,
height: 48,
child: ref
.watch(packageIconProvider(package.packageName))
.when(
data: (data) => Image(image: data),
error: (error, _) => const Icon(Icons.error),
loading: () => const Center(
child: CircularProgressIndicator(),
),
),
),
);
},
itemCount: packages.length,
),
AsyncLoading() => const SliverLoadingBodyPlaceholder(),
AsyncError(:final error) =>
SliverErrorBodyPlaceholder(error.toString()),
_ => const SliverToBoxAdapter(),
},
],
),
);
}
}

View File

@@ -1,2 +1,3 @@
export 'config_options_page.dart';
export 'per_app_proxy_page.dart';
export 'settings_page.dart';

View File

@@ -1,8 +1,11 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/core/router/routes/routes.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class AdvancedSettingTiles extends HookConsumerWidget {
@@ -13,6 +16,7 @@ class AdvancedSettingTiles extends HookConsumerWidget {
final t = ref.watch(translationsProvider);
final debug = ref.watch(debugModeNotifierProvider);
final perAppProxy = ref.watch(perAppProxyModeNotifierProvider).enabled;
return Column(
children: [
@@ -23,6 +27,33 @@ class AdvancedSettingTiles extends HookConsumerWidget {
await const ConfigOptionsRoute().push(context);
},
),
if (Platform.isAndroid) ...[
ListTile(
title: Text(t.settings.network.perAppProxyPageTitle),
leading: const Icon(Icons.apps),
trailing: Switch(
value: perAppProxy,
onChanged: (value) async {
final newMode =
perAppProxy ? PerAppProxyMode.off : PerAppProxyMode.exclude;
await ref
.read(perAppProxyModeNotifierProvider.notifier)
.update(newMode);
if (!perAppProxy && context.mounted) {
await const PerAppProxyRoute().push(context);
}
},
),
onTap: () async {
if (!perAppProxy) {
await ref
.read(perAppProxyModeNotifierProvider.notifier)
.update(PerAppProxyMode.exclude);
}
if (context.mounted) await const PerAppProxyRoute().push(context);
},
),
],
SwitchListTile(
title: Text(t.settings.advanced.debugMode),
value: debug,

View File

@@ -1,7 +1,13 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:fpdart/fpdart.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/utils/utils.dart';
part 'platform_settings.freezed.dart';
part 'platform_settings.g.dart';
class PlatformSettings with InfraLogger {
late final MethodChannel _methodChannel =
const MethodChannel("com.hiddify.app/platform.settings");
@@ -29,4 +35,55 @@ class PlatformSettings with InfraLogger {
},
);
}
TaskEither<String, List<InstalledPackageInfo>> getInstalledPackages() {
return TaskEither(
() async {
loggy.debug("getting installed packages info");
final result =
await _methodChannel.invokeMethod<String>("get_installed_packages");
if (result == null) return left("null response");
return right(
(jsonDecode(result) as List).map((e) {
return InstalledPackageInfo.fromJson(e as Map<String, dynamic>);
}).toList(),
);
},
);
}
TaskEither<String, Uint8List> getPackageIcon(
String packageName,
) {
return TaskEither(
() async {
loggy.debug("getting package [$packageName] icon");
final result = await _methodChannel.invokeMethod<String>(
"get_package_icon",
{"packageName": packageName},
);
if (result == null) return left("null response");
final Uint8List decoded;
try {
decoded = base64.decode(result);
} catch (e) {
return left("error parsing base64 response");
}
return right(decoded);
},
);
}
}
@freezed
class InstalledPackageInfo with _$InstalledPackageInfo {
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory InstalledPackageInfo({
required String packageName,
required String name,
required bool isSystemApp,
}) = _InstalledPackageInfo;
factory InstalledPackageInfo.fromJson(Map<String, dynamic> json) =>
_$InstalledPackageInfoFromJson(json);
}