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