diff --git a/src/Common.ClientLib.Droid/Extensions/ContextExtensions.cs b/src/Common.ClientLib.Droid/Extensions/ContextExtensions.cs
index f292c5fb0..833282e71 100644
--- a/src/Common.ClientLib.Droid/Extensions/ContextExtensions.cs
+++ b/src/Common.ClientLib.Droid/Extensions/ContextExtensions.cs
@@ -1,5 +1,7 @@
+using Android.App;
using Android.Content;
using Android.Graphics.Drawables;
+using Android.OS;
using Android.Runtime;
using AndroidX.Annotations;
using AndroidX.Core.Content;
@@ -8,7 +10,7 @@ using AndroidApplication = Android.App.Application;
// ReSharper disable once CheckNamespace
namespace System
{
- public static class ContextExtensions
+ public static partial class ContextExtensions
{
///
public static TService GetSystemService(this Context context) where TService : class, IJavaObject
diff --git a/src/Common.ClientLib/Application/Services/NotificationImportanceLevel.cs b/src/Common.ClientLib/Application/Services/NotificationImportanceLevel.cs
index 8c7e6af03..a8eb5cac6 100644
--- a/src/Common.ClientLib/Application/Services/NotificationImportanceLevel.cs
+++ b/src/Common.ClientLib/Application/Services/NotificationImportanceLevel.cs
@@ -1,4 +1,4 @@
-namespace System.Application.Services
+namespace System.Application.Services
{
///
/// 通知重要性级别
diff --git a/src/ST.Client.Android/Extensions/ContextExtensions.2.cs b/src/ST.Client.Android/Extensions/ContextExtensions.2.cs
new file mode 100644
index 000000000..8b21fa0fb
--- /dev/null
+++ b/src/ST.Client.Android/Extensions/ContextExtensions.2.cs
@@ -0,0 +1,32 @@
+using Android.App;
+using Android.Content;
+using Android.OS;
+using System.Application.Services.Native;
+using NativeService = System.Application.Services.Native.IServiceBase;
+
+// ReSharper disable once CheckNamespace
+namespace System
+{
+ partial class ContextExtensions
+ {
+ ///
+ /// 启动或关闭前台服务
+ ///
+ ///
+ ///
+ ///
+ public static void CallForegroundService(this Context context, bool startOrStop) where TService : Service, IServiceBase
+ {
+ Intent intent = new(context, typeof(TService));
+ intent.SetAction(startOrStop ? NativeService.START : NativeService.STOP);
+ if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
+ {
+ context.StartForegroundService(intent);
+ }
+ else
+ {
+ context.StartService(intent);
+ }
+ }
+ }
+}
diff --git a/src/ST.Client.Android/Properties/Permissions.cs b/src/ST.Client.Android/Properties/Permissions.cs
index a33e9faad..e05efcc5c 100644
--- a/src/ST.Client.Android/Properties/Permissions.cs
+++ b/src/ST.Client.Android/Properties/Permissions.cs
@@ -25,4 +25,8 @@ using static Android.Manifest.Permission;
[assembly: UsesPermission(Vibrate)] // android.permission.VIBRATE
[assembly: UsesPermission(Flashlight)] // android.permission.FLASHLIGHT
[assembly: UsesPermission(Camera)] // android.permission.CAMERA
-#endregion
\ No newline at end of file
+#endregion
+
+// 前台服务
+// https://developer.android.google.cn/about/versions/pie/android-9.0-changes-28?hl=zh_cn
+[assembly: UsesPermission(ForegroundService)] // android.permission.FOREGROUND_SERVICE
\ No newline at end of file
diff --git a/src/ST.Client.Android/Services/Implementation/AndroidNotificationServiceImpl.cs b/src/ST.Client.Android/Services/Implementation/AndroidNotificationServiceImpl.cs
index 92ebc60f0..58058a017 100644
--- a/src/ST.Client.Android/Services/Implementation/AndroidNotificationServiceImpl.cs
+++ b/src/ST.Client.Android/Services/Implementation/AndroidNotificationServiceImpl.cs
@@ -3,16 +3,20 @@ using Android.Content;
using Android.OS;
using AndroidX.Core.App;
using System.Application.UI;
+using System.Application.UI.Resx;
using System.Collections.Generic;
using AndroidApplication = Android.App.Application;
using CC = System.Common.Constants;
using JClass = Java.Lang.Class;
+using NativeService = System.Application.Services.Native.IServiceBase;
namespace System.Application.Services.Implementation
{
///
internal sealed class AndroidNotificationServiceImpl : INotificationService
{
+ public static AndroidNotificationServiceImpl Instance => (AndroidNotificationServiceImpl)INotificationService.Instance;
+
readonly IAndroidApplication app;
public AndroidNotificationServiceImpl(IAndroidApplication app)
{
@@ -22,6 +26,12 @@ namespace System.Application.Services.Implementation
public static bool IsSupportedNotificationChannel
=> Build.VERSION.SdkInt >= BuildVersionCodes.O;
+ JClass? GetEntrance(Entrance entrance)
+ {
+ var notificationEntrance = (entrance != default ? GetActivityType(entrance)?.GetJClass() : null) ?? app.NotificationEntrance;
+ return notificationEntrance;
+ }
+
Type? GetActivityType(Entrance entrance) => entrance switch
{
Entrance.Main => app.MainActivityType,
@@ -97,10 +107,8 @@ namespace System.Application.Services.Implementation
var name = notificationChannelType.GetName();
var description = notificationChannelType.GetDescription();
var level = GetNotificationImportance(notificationChannelType.GetImportanceLevel());
- notificationChannel = new NotificationChannel(channelId, name, level)
- {
- Description = description,
- };
+ notificationChannel = new NotificationChannel(channelId, name, level);
+ if (!string.IsNullOrWhiteSpace(description)) notificationChannel.Description = description;
CreateNotificationChannel(notificationChannelType, notificationChannel);
manager.CreateNotificationChannel(notificationChannel);
}
@@ -137,17 +145,19 @@ namespace System.Application.Services.Implementation
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null),
};
- NotificationCompat.Builder BuildNotify(
+ static NotificationCompat.Builder BuildNotify(
Context context,
- NotificationManagerCompat manager,
+ NotificationManagerCompat? manager,
string text,
NotificationType notificationType,
bool? autoCancel = null,
string? title = null,
JClass? entrance = null,
+ string? entranceAction = null,
IReadOnlyCollection? actions = null)
{
var channelType = notificationType.GetChannel();
+ manager ??= NotificationManagerCompat.From(context);
CreateNotificationChannel(manager, channelType, out var channelId);
var builder = new NotificationCompat.Builder(context, channelId);
var level = channelType.GetImportanceLevel();
@@ -168,7 +178,8 @@ namespace System.Application.Services.Implementation
if (entrance != null)
{
var intent = new Intent(context, entrance);
- var pendingIntent = PendingIntent.GetActivity(context, 0, intent, 0);
+ if (!string.IsNullOrWhiteSpace(entranceAction)) intent.SetAction(entranceAction);
+ var pendingIntent = PendingIntent.GetActivity(context, 0, intent, PendingIntentFlags.Immutable | PendingIntentFlags.UpdateCurrent);
builder.SetContentIntent(pendingIntent);
}
return builder;
@@ -180,7 +191,7 @@ namespace System.Application.Services.Implementation
string? title,
Entrance entrance)
{
- var notificationEntrance = (entrance != default ? GetActivityType(entrance)?.GetJClass() : null) ?? app.NotificationEntrance;
+ var notificationEntrance = GetEntrance(entrance);
var context = AndroidApplication.Context;
var manager = NotificationManagerCompat.From(context);
var builder = BuildNotify(context, manager, text, notificationType,
@@ -251,5 +262,50 @@ namespace System.Application.Services.Implementation
}
return new Progress(Handler);
}
+
+ ///
+ /// 停止服务按钮
+ ///
+ ///
+ ///
+ NotificationCompat.Action BuildStopServiceAction(Service service)
+ {
+ var intent = new Intent(service, service.GetType());
+ intent.SetAction(NativeService.STOP);
+ const int requestCode = 0;
+ const PendingIntentFlags flags = PendingIntentFlags.Immutable;
+ var pendingIntent = Build.VERSION.SdkInt >= BuildVersionCodes.O ?
+ PendingIntent.GetForegroundService(service, requestCode, intent, flags) :
+ PendingIntent.GetService(service, requestCode, intent, flags);
+ const int icon = 0;
+ var builder = new NotificationCompat.Action.Builder(icon,
+ AppResources.StopService, pendingIntent);
+ return builder.Build();
+ }
+
+ ///
+ /// 使用通知栏来设置前台服务
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void StartForeground(Service service,
+ NotificationType notificationType,
+ string text,
+ string? entranceAction = null)
+ {
+ var stopAction = BuildStopServiceAction(service);
+ var builder = BuildNotify(service, null,
+ text, notificationType,
+ entrance: GetEntrance(Entrance.Main),
+ actions: new NotificationCompat.Action[] {
+ stopAction,
+ },
+ entranceAction: entranceAction);
+ var notification = builder.Build();
+ var notifyId = GetNotifyId(notificationType);
+ service.StartForeground(notifyId, notification);
+ }
}
}
diff --git a/src/ST.Client.Android/Services/Native/CommunityFixVpnService.JNI.tun2http.cs b/src/ST.Client.Android/Services/Native/CommunityFixVpnService.JNI.tun2http.cs
deleted file mode 100644
index b3954a19e..000000000
--- a/src/ST.Client.Android/Services/Native/CommunityFixVpnService.JNI.tun2http.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using Android.Runtime;
-using System.Runtime.InteropServices;
-
-namespace System.Application.Services.Native
-{
- partial class CommunityFixVpnService
- {
- const string tun2http = "tun2http";
-
- [DllImport(tun2http,
- EntryPoint = "Java_tun_proxy_service_Tun2HttpVpnService_jni_1init")]
- static extern void jni_init(IntPtr env, IntPtr thiz);
- [DllImport(tun2http,
- EntryPoint = "Java_tun_proxy_service_Tun2HttpVpnService_jni_1start")]
- static extern void jni_start(IntPtr env, IntPtr thiz, IntPtr tun, IntPtr fwd53, IntPtr rcode, IntPtr proxyIp, IntPtr proxyPort);
- [DllImport(tun2http,
- EntryPoint = "Java_tun_proxy_service_Tun2HttpVpnService_jni_1stop")]
- static extern void jni_stop(IntPtr env, IntPtr thiz, IntPtr tun);
- [DllImport(tun2http,
- EntryPoint = "Java_tun_proxy_service_Tun2HttpVpnService_jni_1get_1mtu")]
- static extern int jni_get_mtu(IntPtr env, IntPtr thiz);
- [DllImport(tun2http,
- EntryPoint = "Java_tun_proxy_service_Tun2HttpVpnService_jni_1done")]
- static extern void jni_done(IntPtr env, IntPtr thiz);
- [DllImport(tun2http,
- EntryPoint = "Java_tun_utils_Util_jni_1getprop")]
- static extern string jni_getprop(IntPtr env, IntPtr thiz, IntPtr name);
-
-#pragma warning disable IDE1006 // 命名样式
- void jni_init() => jni_init(JNIEnv.Handle, Handle);
- void jni_start(int tun, bool fwd53, int rcode, string proxyIp, int proxyPort)
- {
- var tun_ = new Java.Lang.Integer(tun);
- var fwd53_ = new Java.Lang.Boolean(fwd53);
- var rcode_ = new Java.Lang.Integer(rcode);
- var proxyIp_ = new Java.Lang.String(proxyIp);
- var proxyPort_ = new Java.Lang.Integer(proxyPort);
- jni_start(JNIEnv.Handle, Handle, tun_.Handle, fwd53_.Handle, rcode_.Handle, proxyIp_.Handle, proxyPort_.Handle);
- }
- void jni_stop(int tun)
- {
- var tun_ = new Java.Lang.Integer(tun);
- jni_stop(JNIEnv.Handle, Handle, tun_.Handle);
- }
- int jni_get_mtu() => jni_get_mtu(JNIEnv.Handle, Handle);
- void jni_done() => jni_init(JNIEnv.Handle, Handle);
- string jni_getprop(string name)
- {
- var name_ = new Java.Lang.String(name);
- return jni_getprop(JNIEnv.Handle, Handle, name_.Handle);
- }
-#pragma warning restore IDE1006 // 命名样式
- }
-}
diff --git a/src/ST.Client.Android/Services/Native/CommunityFixVpnService.cs b/src/ST.Client.Android/Services/Native/CommunityFixVpnService.cs
deleted file mode 100644
index 19d9349d7..000000000
--- a/src/ST.Client.Android/Services/Native/CommunityFixVpnService.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-using Android;
-using Android.App;
-using Android.Content;
-using Android.Net;
-using Android.OS;
-using Android.Runtime;
-using System.Application.Services.Native;
-using System.Net;
-using static AndroidX.Activity.Result.ActivityResultTask;
-using XEPlatform = Xamarin.Essentials.Platform;
-using static System.Properties.ThisAssembly;
-using System.Application.Settings;
-
-namespace System.Application.Services.Native
-{
- ///
- /// 使用 虚拟专用网 (VPN) 进行网络加速
- /// https://developer.android.google.cn/guide/topics/connectivity/vpn?hl=zh_cn
- ///
- [Register(JavaPackageConstants.Services + nameof(CommunityFixVpnService))]
- [Service(Permission = Manifest.Permission.BindVpnService, Exported = true)]
- [IntentFilter(new[] { "android.net.VpnService" })]
- internal sealed partial class CommunityFixVpnService : VpnService
- {
- const string TAG = nameof(CommunityFixVpnService);
-
- ///
- /// 启动 VPN 服务
- ///
- ///
- ///
- static void StartCore(Activity activity, bool startOrStop, IPAddress? ip, int port)
- {
- var intent = GetServiceIntent(activity)
- .SetAction(startOrStop ? ACTION_CONNECT : ACTION_DISCONNECT);
- if (startOrStop)
- {
- intent.PutExtra(nameof(IPEndPoint.Address), ip?.ToString());
- intent.PutExtra(nameof(IPEndPoint.Port), port);
- }
- activity.StartService(intent);
- static Intent GetServiceIntent(Context context)
- => new(context, typeof(CommunityFixVpnService));
- }
-
- ///
- /// 调用 VpnService.prepare() 以询问权限(需要时)与 启动 VPN 服务
- ///
- ///
- public static async void Start(Activity activity, bool startOrStop = true, IPAddress? ip = null, int port = default)
- {
- if (startOrStop)
- {
- // 调用 VpnService.prepare() 以询问权限(需要时)
- var intent = Prepare(activity);
- if (intent != null)
- {
- void OnResult(Intent intent) => StartCore(activity, true, ip, port);
- await IntermediateActivity.StartAsync(intent, requestCodeVpnService, onResult: OnResult);
- return;
- }
- }
- // 不需要授权则直接启动
- StartCore(activity, startOrStop, ip, port);
- }
-
- const string ACTION_CONNECT = JavaPackageConstants.Services + nameof(CommunityFixVpnService) + ".START";
- const string ACTION_DISCONNECT = JavaPackageConstants.Services + nameof(CommunityFixVpnService) + ".STOP";
-
- ParcelFileDescriptor? localTunnel;
- string? address;
- int port;
-
- public override void OnCreate()
- {
- jni_init();
- base.OnCreate();
- }
-
- [return: GeneratedEnum]
- public override StartCommandResult OnStartCommand(Intent? intent, [GeneratedEnum] StartCommandFlags flags, int startId)
- {
- if (intent != null)
- {
- if (ACTION_DISCONNECT.Equals(intent.Action))
- {
- Disconnect();
- }
- else
- {
- address = intent.GetStringExtra(nameof(IPEndPoint.Address));
- port = intent.GetIntExtra(nameof(IPEndPoint.Port), default);
- Connect();
- }
- }
- return StartCommandResult.NotSticky; // 重启该服务还需启动代理服务
- }
-
- void Connect()
- {
- if (localTunnel != null) return;
-
- // Configure a new interface from our VpnService instance. This must be done
- // from inside a VpnService.
- var builder = new Builder(this).SetSession(AssemblyTrademark);
-
- // Create a local TUN interface using predetermined addresses. In your app,
- // you typically use values returned from the VPN gateway during handshaking.
- builder.AddAddress("10.1.10.1", 32);
- //builder.AddAddress("fd00:1:fd00:1:fd00:1:fd00:1", 128);
- builder.AddRoute("0.0.0.0", 0);
- //builder.AddRoute("0:0:0:0:0:0:0:0", 0);
-
- var dnss = GetDefaultDNS();
- Array.ForEach(dnss, x => builder.AddDnsServer(x));
-
- int mtu = jni_get_mtu();
- builder.SetMtu(mtu);
-
- var pkg = PackageName!;
- builder.AddDisallowedApplication(pkg);
-
- localTunnel = builder.Establish()!;
-
- jni_start(localTunnel.Fd, false, 3, address!, port);
- }
-
- void Disconnect()
- {
- if (localTunnel != null)
- {
- try
- {
- jni_stop(localTunnel.Fd);
- }
- catch (Java.Lang.Throwable ex)
- {
- Log.Error(TAG, ex, "jni_stop");
- jni_stop(-1);
- }
- try
- {
- localTunnel.Close();
- }
- catch (Java.IO.IOException ex)
- {
- Log.Error(TAG, ex, "localTunnel.Close");
- }
- localTunnel.Dispose();
- localTunnel = null;
- }
- StopForeground(true);
- }
-
- public override void OnRevoke()
- {
- Disconnect();
- base.OnRevoke();
- }
-
- public override void OnDestroy()
- {
- Disconnect();
- jni_done();
- base.OnDestroy();
- }
-
- string[] GetDefaultDNS()
- {
- //return new[] {
- // IDnsAnalysisService.PrimaryDNS_Ali,
- // IDnsAnalysisService.SecondaryDNS_Ali,
- //};
- var context = Android.App.Application.Context;
- string? dns1 = null, dns2 = null;
- if (Build.VERSION.SdkInt > BuildVersionCodes.NMr1)
- {
- var cm = context.GetSystemService();
- var an = cm.ActiveNetwork;
- if (an != null)
- {
- var lp = cm.GetLinkProperties(an);
- if (lp != null)
- {
- var dns = lp.DnsServers;
- if (dns != null)
- {
- if (dns.Count > 0)
- dns1 = dns[0].HostAddress;
- if (dns.Count > 1)
- dns2 = dns[1].HostAddress;
- }
- }
- }
- }
- else
- {
- dns1 = jni_getprop("net.dns1");
- dns2 = jni_getprop("net.dns2");
- }
- return new[]
- {
- string.IsNullOrEmpty(dns1) ? IDnsAnalysisService.PrimaryDNS_Ali : dns1!,
- string.IsNullOrEmpty(dns2) ? IDnsAnalysisService.SecondaryDNS_Ali : dns2!,
- };
- }
- }
-}
-
-namespace System.Application.Services.Implementation
-{
- partial class AndroidPlatformServiceImpl
- {
- bool IPlatformService.SetAsSystemProxy(bool state, IPAddress? ip, int port)
- {
- if (ProxySettings.IsVpnMode.Value)
- {
- var activity = XEPlatform.CurrentActivity;
- CommunityFixVpnService.Start(activity, state, ip, port);
- }
-#if DEBUG
- Toast.Show($"SystemProxy: {ip}:{port}");
-#endif
- return true;
- }
- }
-}
\ No newline at end of file
diff --git a/src/ST.Client.Android/Services/Native/ProxyForegroundService.JNI.tun2http.cs b/src/ST.Client.Android/Services/Native/ProxyForegroundService.JNI.tun2http.cs
new file mode 100644
index 000000000..7eba27801
--- /dev/null
+++ b/src/ST.Client.Android/Services/Native/ProxyForegroundService.JNI.tun2http.cs
@@ -0,0 +1,57 @@
+using Android.Runtime;
+using System.Runtime.InteropServices;
+
+namespace System.Application.Services.Native
+{
+ partial class ProxyForegroundService
+ {
+ partial class VpnService
+ {
+ const string tun2http = "tun2http";
+
+ [DllImport(tun2http,
+ EntryPoint = "Java_tun_proxy_service_Tun2HttpVpnService_jni_1init")]
+ static extern void jni_init(IntPtr env, IntPtr thiz);
+ [DllImport(tun2http,
+ EntryPoint = "Java_tun_proxy_service_Tun2HttpVpnService_jni_1start")]
+ static extern void jni_start(IntPtr env, IntPtr thiz, IntPtr tun, IntPtr fwd53, IntPtr rcode, IntPtr proxyIp, IntPtr proxyPort);
+ [DllImport(tun2http,
+ EntryPoint = "Java_tun_proxy_service_Tun2HttpVpnService_jni_1stop")]
+ static extern void jni_stop(IntPtr env, IntPtr thiz, IntPtr tun);
+ [DllImport(tun2http,
+ EntryPoint = "Java_tun_proxy_service_Tun2HttpVpnService_jni_1get_1mtu")]
+ static extern int jni_get_mtu(IntPtr env, IntPtr thiz);
+ [DllImport(tun2http,
+ EntryPoint = "Java_tun_proxy_service_Tun2HttpVpnService_jni_1done")]
+ static extern void jni_done(IntPtr env, IntPtr thiz);
+ [DllImport(tun2http,
+ EntryPoint = "Java_tun_utils_Util_jni_1getprop")]
+ static extern string jni_getprop(IntPtr env, IntPtr thiz, IntPtr name);
+
+#pragma warning disable IDE1006 // 命名样式
+ void jni_init() => jni_init(JNIEnv.Handle, Handle);
+ void jni_start(int tun, bool fwd53, int rcode, string proxyIp, int proxyPort)
+ {
+ var tun_ = new Java.Lang.Integer(tun);
+ var fwd53_ = new Java.Lang.Boolean(fwd53);
+ var rcode_ = new Java.Lang.Integer(rcode);
+ var proxyIp_ = new Java.Lang.String(proxyIp);
+ var proxyPort_ = new Java.Lang.Integer(proxyPort);
+ jni_start(JNIEnv.Handle, Handle, tun_.Handle, fwd53_.Handle, rcode_.Handle, proxyIp_.Handle, proxyPort_.Handle);
+ }
+ void jni_stop(int tun)
+ {
+ var tun_ = new Java.Lang.Integer(tun);
+ jni_stop(JNIEnv.Handle, Handle, tun_.Handle);
+ }
+ int jni_get_mtu() => jni_get_mtu(JNIEnv.Handle, Handle);
+ void jni_done() => jni_init(JNIEnv.Handle, Handle);
+ string jni_getprop(string name)
+ {
+ var name_ = new Java.Lang.String(name);
+ return jni_getprop(JNIEnv.Handle, Handle, name_.Handle);
+ }
+#pragma warning restore IDE1006 // 命名样式
+ }
+ }
+}
diff --git a/src/ST.Client.Android/Services/Native/ProxyForegroundService.cs b/src/ST.Client.Android/Services/Native/ProxyForegroundService.cs
new file mode 100644
index 000000000..c5b5480d5
--- /dev/null
+++ b/src/ST.Client.Android/Services/Native/ProxyForegroundService.cs
@@ -0,0 +1,286 @@
+using Android;
+using Android.App;
+using Android.Content;
+using Android.Net;
+using Android.OS;
+using Android.Runtime;
+using System.Net;
+using System.Application.Settings;
+using System.Application.UI.Resx;
+using System.Application.UI.ViewModels;
+using System.Application.Services.Implementation;
+using static AndroidX.Activity.Result.ActivityResultTask;
+using static System.Properties.ThisAssembly;
+using static System.Application.Services.Native.IServiceBase;
+using System.Threading.Tasks;
+using AndroidNetVpnService = Android.Net.VpnService;
+
+// https://docs.microsoft.com/zh-cn/xamarin/android/app-fundamentals/services/creating-a-service/started-services
+
+namespace System.Application.Services.Native
+{
+ static partial class ProxyForegroundService
+ {
+ public static async void Call(Activity activity, bool startOrStop)
+ {
+ if (ProxySettings.IsVpnMode.Value)
+ {
+ if (startOrStop)
+ {
+ // 调用 VpnService.prepare() 以询问权限(需要时)
+ var intent = AndroidNetVpnService.Prepare(activity);
+ if (intent != null)
+ {
+ void OnResult(Intent intent)
+ => activity.CallForegroundService(true);
+ await IntermediateActivity.StartAsync(intent,
+ requestCodeVpnService, onResult: OnResult);
+ return;
+ }
+ }
+ // 不需要授权则直接启动
+ activity.CallForegroundService(startOrStop);
+ }
+ else
+ {
+ activity.CallForegroundService(startOrStop);
+ }
+ }
+
+ ///
+ /// 用于本地加速的代理前台服务
+ ///
+ [Register(JavaPackageConstants.Services + TAG)]
+ [Service]
+ partial class Service : ForegroundService
+ {
+ const string TAG = nameof(ProxyForegroundService);
+
+ public override void OnStart()
+ {
+ base.OnStart();
+ ProxyService.Current.ProxyStatus = true;
+ }
+
+ public override void OnStop()
+ {
+ base.OnStop();
+ ProxyService.Current.ProxyStatus = false;
+ }
+ }
+
+ ///
+ /// 使用 虚拟专用网 (VPN) 进行本地加速的代理前台服务
+ /// https://developer.android.google.cn/guide/topics/connectivity/vpn?hl=zh_cn
+ ///
+ [Register(JavaPackageConstants.Services + TAG)]
+ [Service(Permission = Manifest.Permission.BindVpnService, Exported = true)]
+ [IntentFilter(new[] { "android.net.VpnService" })]
+ sealed partial class VpnService : AndroidNetVpnService
+ {
+ const string TAG = nameof(ProxyForegroundService) + "_VPN";
+
+ bool isStart;
+ ParcelFileDescriptor? localTunnel;
+
+ public override void OnCreate()
+ {
+ jni_init();
+ base.OnCreate();
+ }
+
+ public /*async*/ void OnStart()
+ {
+ if (isStart) return;
+ isStart = true;
+
+ IForegroundService f = this;
+ AndroidNotificationServiceImpl.Instance.StartForeground(this, f.NotificationType, f.NotificationText, f.NotificationEntranceAction);
+
+ ProxyService.Current.ProxyStatus = true;
+
+ // wait ???
+ //await Task.Delay(3500);
+
+ if (localTunnel != null) return;
+
+ // Configure a new interface from our VpnService instance. This must be done
+ // from inside a VpnService.
+ var builder = new Builder(this).SetSession(AssemblyTrademark);
+
+ // Create a local TUN interface using predetermined addresses. In your app,
+ // you typically use values returned from the VPN gateway during handshaking.
+ builder.AddAddress("10.1.10.1", 32);
+ //builder.AddAddress("fd00:1:fd00:1:fd00:1:fd00:1", 128);
+ builder.AddRoute("0.0.0.0", 0);
+ //builder.AddRoute("0:0:0:0:0:0:0:0", 0);
+
+ var dnss = GetDefaultDNS();
+ Array.ForEach(dnss, x => builder.AddDnsServer(x));
+
+ int mtu = jni_get_mtu();
+ builder.SetMtu(mtu);
+
+ var pkg = PackageName!;
+ builder.AddDisallowedApplication(pkg);
+
+ localTunnel = builder.Establish()!;
+
+ var httpProxyService = IHttpProxyService.Instance;
+ var address = httpProxyService.ProxyIp;
+ var port = httpProxyService.ProxyPort;
+ jni_start(localTunnel.Fd, false, 3, address!.ToString(), port);
+ }
+
+ public void OnStop()
+ {
+ if (!isStart) return;
+ isStart = false;
+
+ StopForeground(true);
+
+ if (localTunnel != null)
+ {
+ try
+ {
+ jni_stop(localTunnel.Fd);
+ }
+ catch (Java.Lang.Throwable ex)
+ {
+ Log.Error(TAG, ex, "jni_stop");
+ jni_stop(-1);
+ }
+ try
+ {
+ localTunnel.Close();
+ }
+ catch (Java.IO.IOException ex)
+ {
+ Log.Error(TAG, ex, "localTunnel.Close");
+ }
+ localTunnel.Dispose();
+ localTunnel = null;
+ }
+
+ ProxyService.Current.ProxyStatus = false;
+ }
+
+ [return: GeneratedEnum]
+ public override StartCommandResult OnStartCommand(Intent? intent, [GeneratedEnum] StartCommandFlags flags, int startId)
+ {
+ if (intent != null)
+ {
+ var action = intent.Action;
+ switch (action)
+ {
+ case START:
+ OnStart();
+ return StartCommandResult.RedeliverIntent;
+ case STOP:
+ default:
+ OnStop();
+ StopSelf();
+ break;
+ }
+ }
+ return StartCommandResult.NotSticky;
+ }
+
+ public override void OnRevoke()
+ {
+ OnStop();
+ base.OnRevoke();
+ }
+
+ public override void OnDestroy()
+ {
+ OnStop();
+ base.OnDestroy();
+ }
+
+ string[] GetDefaultDNS()
+ {
+ //return new[] {
+ // IDnsAnalysisService.PrimaryDNS_Ali,
+ // IDnsAnalysisService.SecondaryDNS_Ali,
+ //};
+ var context = Android.App.Application.Context;
+ string? dns1 = null, dns2 = null;
+ if (Build.VERSION.SdkInt > BuildVersionCodes.NMr1)
+ {
+ var cm = context.GetSystemService();
+ var an = cm.ActiveNetwork;
+ if (an != null)
+ {
+ var lp = cm.GetLinkProperties(an);
+ if (lp != null)
+ {
+ var dns = lp.DnsServers;
+ if (dns != null)
+ {
+ if (dns.Count > 0)
+ dns1 = dns[0].HostAddress;
+ if (dns.Count > 1)
+ dns2 = dns[1].HostAddress;
+ }
+ }
+ }
+ }
+ else
+ {
+ dns1 = jni_getprop("net.dns1");
+ dns2 = jni_getprop("net.dns2");
+ }
+ return new[]
+ {
+ string.IsNullOrEmpty(dns1) ? IDnsAnalysisService.PrimaryDNS_Ali : dns1!,
+ string.IsNullOrEmpty(dns2) ? IDnsAnalysisService.SecondaryDNS_Ali : dns2!,
+ };
+ }
+ }
+
+ const NotificationType notificationType
+ = NotificationType.ProxyForegroundService;
+ static string NotificationText
+ => AppResources.ProxyForegroundService_NotificationText;
+ static string? NotificationEntranceAction
+ => nameof(TabItemViewModel.TabItemId.CommunityProxy);
+
+ partial class Service
+ {
+ public override NotificationType NotificationType
+ => notificationType;
+
+ public override string NotificationText
+ => ProxyForegroundService.NotificationText;
+
+ public override string? NotificationEntranceAction
+ => ProxyForegroundService.NotificationEntranceAction;
+ }
+
+ partial class VpnService : IForegroundService
+ {
+ public NotificationType NotificationType => notificationType;
+
+ public string NotificationText
+ => ProxyForegroundService.NotificationText;
+
+ public string? NotificationEntranceAction
+ => ProxyForegroundService.NotificationEntranceAction;
+ }
+ }
+}
+
+namespace System.Application.Services.Implementation
+{
+ partial class AndroidPlatformServiceImpl
+ {
+ bool IPlatformService.SetAsSystemProxy(bool state, IPAddress? ip, int port)
+ {
+#if DEBUG
+ Toast.Show($"SystemProxy: {ip}:{port}");
+#endif
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ST.Client.Android/Services/Native/ServiceBase.cs b/src/ST.Client.Android/Services/Native/ServiceBase.cs
new file mode 100644
index 000000000..772c5c636
--- /dev/null
+++ b/src/ST.Client.Android/Services/Native/ServiceBase.cs
@@ -0,0 +1,113 @@
+using Android.App;
+using Android.Content;
+using Android.OS;
+using Android.Runtime;
+using System.Application.Services.Implementation;
+using static System.Application.Services.Native.IServiceBase;
+
+namespace System.Application.Services.Native
+{
+ ///
+ /// 平台原生服务接口
+ ///
+ public interface IServiceBase
+ {
+ ///
+ /// 启动服务参数值
+ ///
+ public const string START = JavaPackageConstants.Services + nameof(IServiceBase) + ".START";
+
+ ///
+ /// 停止服务参数值
+ ///
+ public const string STOP = JavaPackageConstants.Services + nameof(IServiceBase) + ".STOP";
+
+ ///
+ /// 当服务启动时
+ ///
+ void OnStart();
+
+ ///
+ /// 当服务停止时
+ ///
+ void OnStop();
+ }
+
+ ///
+ /// 平台原生服务基类
+ ///
+ public abstract class ServiceBase : Service, IServiceBase
+ {
+ public abstract void OnStart();
+
+ public abstract void OnStop();
+
+ [return: GeneratedEnum]
+ public override StartCommandResult OnStartCommand(Intent? intent, [GeneratedEnum] StartCommandFlags flags, int startId)
+ {
+ if (intent != null)
+ {
+ var action = intent.Action;
+ switch (action)
+ {
+ case START:
+ OnStart();
+ return StartCommandResult.RedeliverIntent;
+ case STOP:
+ default:
+ StopSelf();
+ break;
+ }
+ }
+ return StartCommandResult.NotSticky;
+ }
+
+ public override void OnDestroy()
+ {
+ OnStop();
+ base.OnDestroy();
+ }
+
+ public override IBinder? OnBind(Intent? intent) => default;
+ }
+
+ ///
+ /// 平台原生前台服务接口
+ ///
+ public interface IForegroundService : IServiceBase
+ {
+ ///
+ /// 通知栏显示的通知类型
+ ///
+ NotificationType NotificationType { get; }
+
+ ///
+ /// 通知栏显示的通知文本
+ ///
+ string NotificationText { get; }
+
+ ///
+ /// 通知栏点击启动活动的参数传递
+ ///
+ string? NotificationEntranceAction { get; }
+ }
+
+ public abstract class ForegroundService : ServiceBase, IForegroundService
+ {
+ public abstract NotificationType NotificationType { get; }
+
+ public abstract string NotificationText { get; }
+
+ public virtual string? NotificationEntranceAction => null;
+
+ public override void OnStart()
+ {
+ AndroidNotificationServiceImpl.Instance.StartForeground(this, NotificationType, NotificationText, NotificationEntranceAction);
+ }
+
+ public override void OnStop()
+ {
+ StopForeground(true);
+ }
+ }
+}
diff --git a/src/ST.Client.Android/UI/Activities/MainActivity.cs b/src/ST.Client.Android/UI/Activities/MainActivity.cs
index d13cc72a4..1ac81b5fc 100644
--- a/src/ST.Client.Android/UI/Activities/MainActivity.cs
+++ b/src/ST.Client.Android/UI/Activities/MainActivity.cs
@@ -1,4 +1,5 @@
using Android.App;
+using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
@@ -74,6 +75,31 @@ namespace System.Application.UI.Activities
NavigationUI.SetupWithNavController(binding!.nav_view, navController);
}
+ protected override void OnNewIntent(Intent? intent)
+ {
+ base.OnNewIntent(intent);
+
+ var action = intent?.Action;
+ if (!string.IsNullOrWhiteSpace(action))
+ {
+ if (navController == null) return;
+ if (Enum.TryParse(action, out var tabItem))
+ {
+ var id = tabItem switch
+ {
+ TabItemViewModel.TabItemId.CommunityProxy => Resource.Id.navigation_community_fix,
+ TabItemViewModel.TabItemId.LocalAuth => Resource.Id.navigation_local_auth,
+ TabItemViewModel.TabItemId.ArchiSteamFarmPlus => Resource.Id.navigation_asf_plus,
+ _ => navController.CurrentDestination.Id,
+ };
+ if (id != navController.CurrentDestination.Id)
+ {
+ navController.Navigate(id);
+ }
+ }
+ }
+ }
+
protected override void OnDestroy()
{
base.OnDestroy();
diff --git a/src/ST.Client.Android/UI/Fragments/CommunityFixFragment.cs b/src/ST.Client.Android/UI/Fragments/CommunityFixFragment.cs
index db23304fa..e3f014fed 100644
--- a/src/ST.Client.Android/UI/Fragments/CommunityFixFragment.cs
+++ b/src/ST.Client.Android/UI/Fragments/CommunityFixFragment.cs
@@ -7,6 +7,7 @@ using AndroidX.SwipeRefreshLayout.Widget;
using Binding;
using ReactiveUI;
using System.Application.Services;
+using System.Application.Services.Native;
using System.Application.Settings;
using System.Application.UI.Adapters;
using System.Application.UI.Adapters.Internals;
@@ -152,11 +153,14 @@ namespace System.Application.UI.Fragments
}), "已知问题");
}
+ void StartProxyButton_Click(bool start)
+ => ProxyForegroundService.Call(RequireActivity(), start);
+
protected override bool OnClick(View view)
{
if (view.Id == Resource.Id.btnStartProxyService)
{
- ViewModel!.StartProxyButton_Click(true);
+ StartProxyButton_Click(true);
const string KEY = "CommunityFixFragment_IsShowTip";
var isShowTip = Preferences2.Get(KEY, false);
if (!isShowTip)
@@ -168,7 +172,7 @@ namespace System.Application.UI.Fragments
}
else if (view.Id == Resource.Id.btnStopProxyService)
{
- ViewModel!.StartProxyButton_Click(false);
+ StartProxyButton_Click(false);
return true;
}
return base.OnClick(view);
diff --git a/src/ST.Client/Extensions/NotificationType_Channel_EnumExtensions.cs b/src/ST.Client/Extensions/NotificationType_Channel_EnumExtensions.cs
index b41e14228..4bc9e6c0a 100644
--- a/src/ST.Client/Extensions/NotificationType_Channel_EnumExtensions.cs
+++ b/src/ST.Client/Extensions/NotificationType_Channel_EnumExtensions.cs
@@ -20,6 +20,9 @@ namespace System.Application
{
NotificationType.Announcement => NotificationChannelType.Announcement,
NotificationType.NewVersion => NotificationChannelType.NewVersion,
+ NotificationType.Message => NotificationChannelType.Message,
+ NotificationType.ProxyForegroundService or
+ NotificationType.ArchiSteamFarmForegroundService => NotificationChannelType.ForegroundService,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
@@ -35,6 +38,8 @@ namespace System.Application
{
NotificationChannelType.Announcement => AppResources.NotificationChannelType_Name_Announcement,
NotificationChannelType.NewVersion => AppResources.NotificationChannelType_Name_NewVersion,
+ NotificationChannelType.Message => AppResources.NotificationChannelType_Name_Message,
+ NotificationChannelType.ForegroundService => AppResources.NotificationChannelType_Name_ForegroundService,
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
}
@@ -53,7 +58,9 @@ namespace System.Application
{
NotificationChannelType.Announcement => AppResources.NotificationChannelType_Description_Announcement,
NotificationChannelType.NewVersion => AppResources.NotificationChannelType_Description_NewVersion,
- _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
+ NotificationChannelType.Message => AppResources.NotificationChannelType_Description_Message,
+ NotificationChannelType.ForegroundService => AppResources.NotificationChannelType_Description_ForegroundService,
+ _ => string.Empty,
};
}
@@ -68,8 +75,7 @@ namespace System.Application
=> value switch
{
NotificationChannelType.Announcement => NotificationImportanceLevel.High,
- NotificationChannelType.NewVersion => NotificationImportanceLevel.Medium,
- _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
+ _ => NotificationImportanceLevel.Medium,
};
}
}
\ No newline at end of file
diff --git a/src/ST.Client/NotificationChannelType.cs b/src/ST.Client/NotificationChannelType.cs
index 7dd56efe6..f2af18037 100644
--- a/src/ST.Client/NotificationChannelType.cs
+++ b/src/ST.Client/NotificationChannelType.cs
@@ -23,16 +23,20 @@ namespace System.Application
///
public enum NotificationChannelType
{
- ///
- /// 新版本
- /// 例如:
- /// 新版本下载进度
- ///
+ ///
NewVersion = 2,
- ///
- /// 公告
- ///
+ ///
Announcement,
+
+ ///
+ Message,
+
+ ///
+ /// Android 前台服务
+ ///
+ ///
+ ///
+ ForegroundService,
}
}
\ No newline at end of file
diff --git a/src/ST.Client/UI/Resx/AppResources.Designer.cs b/src/ST.Client/UI/Resx/AppResources.Designer.cs
index 25ab3dee2..83df620bd 100644
--- a/src/ST.Client/UI/Resx/AppResources.Designer.cs
+++ b/src/ST.Client/UI/Resx/AppResources.Designer.cs
@@ -3662,6 +3662,24 @@ namespace System.Application.UI.Resx {
}
}
+ ///
+ /// 查找类似 当开启前台服务时的通知。 的本地化字符串。
+ ///
+ public static string NotificationChannelType_Description_ForegroundService {
+ get {
+ return ResourceManager.GetString("NotificationChannelType_Description_ForegroundService", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 当有提示消息时的通知。 的本地化字符串。
+ ///
+ public static string NotificationChannelType_Description_Message {
+ get {
+ return ResourceManager.GetString("NotificationChannelType_Description_Message", resourceCulture);
+ }
+ }
+
///
/// 查找类似 当有新版本可用时的提示通知与下载新版本时的进度通知。 的本地化字符串。
///
@@ -3680,6 +3698,24 @@ namespace System.Application.UI.Resx {
}
}
+ ///
+ /// 查找类似 前台服务通知 的本地化字符串。
+ ///
+ public static string NotificationChannelType_Name_ForegroundService {
+ get {
+ return ResourceManager.GetString("NotificationChannelType_Name_ForegroundService", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 消息通知 的本地化字符串。
+ ///
+ public static string NotificationChannelType_Name_Message {
+ get {
+ return ResourceManager.GetString("NotificationChannelType_Name_Message", resourceCulture);
+ }
+ }
+
///
/// 查找类似 新版本通知 的本地化字符串。
///
@@ -3752,6 +3788,15 @@ namespace System.Application.UI.Resx {
}
}
+ ///
+ /// 查找类似 本地加速代理服务正在运行。 的本地化字符串。
+ ///
+ public static string ProxyForegroundService_NotificationText {
+ get {
+ return ResourceManager.GetString("ProxyForegroundService_NotificationText", resourceCulture);
+ }
+ }
+
///
/// 查找类似 恢复代码 的本地化字符串。
///
@@ -5069,6 +5114,15 @@ namespace System.Application.UI.Resx {
}
}
+ ///
+ /// 查找类似 停止服务 的本地化字符串。
+ ///
+ public static string StopService {
+ get {
+ return ResourceManager.GetString("StopService", resourceCulture);
+ }
+ }
+
///
/// 查找类似 {0}成功 的本地化字符串。
///
diff --git a/src/ST.Client/UI/Resx/AppResources.resx b/src/ST.Client/UI/Resx/AppResources.resx
index 0b237c496..8846ca7e5 100644
--- a/src/ST.Client/UI/Resx/AppResources.resx
+++ b/src/ST.Client/UI/Resx/AppResources.resx
@@ -2051,4 +2051,22 @@ AAAAA-BBBBB-CCCC3
重置 Hosts 文件成功
+
+ 当开启前台服务时的通知。
+
+
+ 当有提示消息时的通知。
+
+
+ 前台服务通知
+
+
+ 消息通知
+
+
+ 停止服务
+
+
+ 本地加速代理服务正在运行。
+
\ No newline at end of file
diff --git a/src/ST/NotificationType.cs b/src/ST/NotificationType.cs
index fcc14a8ec..708ad923c 100644
--- a/src/ST/NotificationType.cs
+++ b/src/ST/NotificationType.cs
@@ -22,12 +22,14 @@ namespace System.Application
public enum NotificationType
{
///
- /// 新版本
+ /// 当有新版本可更新时
+ /// 例如:
+ /// 新版本下载进度
///
NewVersion = 1,
///
- /// 公告
+ /// 由服务端下发的公告
///
Announcement,
@@ -35,5 +37,28 @@ namespace System.Application
/// 消息通知
///
Message,
+
+ ///
+ /// 本地加速代理服务(Android 前台服务通知)
+ ///
+ ProxyForegroundService,
+
+ ///
+ /// ASF 服务(Android 前台服务通知)
+ ///
+ ArchiSteamFarmForegroundService,
+
+ /* 添加新的枚举值注意事项:
+ * 需要对该 通知类型 指定一个渠道,多个通知类型可对应一个通知渠道
+ * 在 ST.Client NotificationChannelType
+ * NotificationType_Channel_EnumExtensions.GetChannel(NotificationType)
+ * 如果为新增渠道,则需要增加以下内容
+ * 渠道的用户可见名称,必填
+ * NotificationType_Channel_EnumExtensions.GetName(NotificationChannelType)
+ * 渠道的用户可见描述,可选
+ * NotificationType_Channel_EnumExtensions.GetDescription(NotificationChannelType)
+ * 渠道的重要性级别,可选,默认为 中等(Medium)
+ * NotificationType_Channel_EnumExtensions.GetImportanceLevel(NotificationChannelType)
+ */
}
}
\ No newline at end of file