Files
MeWeb/src/MeWeb.Android/AndroidPlatform.cs
T
thepraandClaude Opus 5.5 50c40195d3
Build / Desktop, server and tools (push) Successful in 1m15s
Release / Every platform, the server, the release (push) Successful in 26m55s
"me-web?" 0.12.0: every address block travels with the app
- The app now carries the whole RIPE Database list of address blocks, all five million, small single-customer blocks
  included, so the provider card and the path can name the block your connection belongs to without a connection to
  the publisher's server. The downloads are about 25 MB larger for it.
- When the publisher's server can't be reached, the provider card still names your network and its block, from the
  public address your router reports or the one the STUN test sees.
- The table is read a small piece at a time, so it takes almost no memory on a phone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-16 19:41:56 +02:00

240 lines
8.1 KiB
C#

using Android.App;
using Android.Content;
using Android.OS;
using Android.Net;
using Android.Net.Wifi;
using Android.Telephony;
using MeWeb.Core.Net;
using Microsoft.Win32.SafeHandles;
namespace MeWeb.Android;
public class AndroidPlatform : SystemPlatform
{
static Context Context => global::Android.App.Application.Context;
public override DeviceKind Device => DeviceKind.Phone;
public override string Name => "android";
public override bool? HasBattery => true;
public override string SystemName => $"Android {Build.VERSION.Release} (API {(int)Build.VERSION.SdkInt}, {System.Runtime.InteropServices.RuntimeInformation.OSArchitecture})";
public override string Model
{
get
{
var maker = Build.Manufacturer?.Trim() ?? "";
var model = Build.Model?.Trim() ?? "";
if (model.Length == 0) return null;
if (maker.Length == 0 || model.StartsWith(maker, StringComparison.OrdinalIgnoreCase)) return model;
return $"{char.ToUpperInvariant(maker[0])}{maker[1..]} {model}";
}
}
public override long MemoryBytes
{
get
{
if (Context.GetSystemService(Context.ActivityService) is not ActivityManager activities) return base.MemoryBytes;
var memory = new ActivityManager.MemoryInfo();
activities.GetMemoryInfo(memory);
return memory.TotalMem > 0 ? memory.TotalMem : base.MemoryBytes;
}
}
public override async Task<CellularReport> ReadCellular(CancellationToken ct)
{
if (Context.GetSystemService(Context.TelephonyService) is not TelephonyManager phone) return null;
var report = new CellularReport
{
Operator = phone.NetworkOperatorName,
NetworkCode = phone.NetworkOperator,
Roaming = phone.IsNetworkRoaming,
};
if (OperatingSystem.IsAndroidVersionAtLeast(28)) report.Carrier = phone.SimCarrierIdName;
if (OperatingSystem.IsAndroidVersionAtLeast(33) && await Allowed(global::Android.Manifest.Permission.ReadBasicPhoneState, ct))
{
try
{
report.Radio = phone.DataNetworkType switch
{
NetworkType.Nr => "5G",
NetworkType.Lte or NetworkType.Iwlan => "4G (LTE)",
NetworkType.Hspap or NetworkType.Hspa or NetworkType.Hsdpa or NetworkType.Hsupa or NetworkType.Umts or NetworkType.Evdo0 or NetworkType.EvdoA or NetworkType.EvdoB or NetworkType.Ehrpd => "3G",
NetworkType.Edge or NetworkType.Gprs or NetworkType.Cdma or NetworkType.OneXrtt or NetworkType.Gsm or NetworkType.Iden => "2G",
_ => null,
};
}
catch (Java.Lang.SecurityException)
{
}
}
if (OperatingSystem.IsAndroidVersionAtLeast(31) && report.Radio is null or "4G (LTE)" && await FiveGShown(phone, ct)) report.Radio = "5G";
return report;
}
[System.Runtime.Versioning.SupportedOSPlatform("android31.0")]
static async Task<bool> FiveGShown(TelephonyManager phone, CancellationToken ct)
{
var listener = new DisplayListener();
try
{
phone.RegisterTelephonyCallback(Context.MainExecutor, listener);
var shown = await listener.Shown.Task.WaitAsync(TimeSpan.FromSeconds(1.5), ct);
return shown is (int)OverrideNetworkType.NrNsa or (int)OverrideNetworkType.NrAdvanced;
}
catch (Exception ex) when (ex is TimeoutException or Java.Lang.SecurityException or Java.Lang.IllegalStateException)
{
return false;
}
finally
{
try
{
phone.UnregisterTelephonyCallback(listener);
}
catch (Java.Lang.IllegalStateException)
{
}
}
}
[System.Runtime.Versioning.SupportedOSPlatform("android31.0")]
sealed class DisplayListener : TelephonyCallback, TelephonyCallback.IDisplayInfoListener
{
public TaskCompletionSource<int> Shown { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
public void OnDisplayInfoChanged(TelephonyDisplayInfo telephonyDisplayInfo) => Shown.TrySetResult((int)telephonyDisplayInfo.OverrideNetworkType);
}
static async Task<bool> Allowed(string permission, CancellationToken ct)
{
if (Context.CheckSelfPermission(permission) == global::Android.Content.PM.Permission.Granted) return true;
var activity = MainActivity.Current;
if (activity is null) return false;
try
{
return await activity.Ask(permission).WaitAsync(TimeSpan.FromMinutes(2), ct);
}
catch (TimeoutException)
{
return false;
}
}
public override bool ShareLink(string url, string title)
{
var send = new Intent(Intent.ActionSend);
send.SetType("text/plain");
send.PutExtra(Intent.ExtraText, url);
send.PutExtra(Intent.ExtraSubject, title);
var chooser = Intent.CreateChooser(send, title);
if (chooser is null) return false;
chooser.AddFlags(ActivityFlags.NewTask);
try
{
Context.StartActivity(chooser);
return true;
}
catch (ActivityNotFoundException)
{
return false;
}
}
public override Stream OpenRegistry()
{
try
{
using var asset = Context.Assets?.OpenFd("registry.bin");
if (asset?.ParcelFileDescriptor is null) return null;
using var copy = asset.ParcelFileDescriptor.Dup();
return new FileStream(new SafeFileHandle(copy.DetachFd(), true), FileAccess.Read) { Position = asset.StartOffset };
}
catch (Exception)
{
return null;
}
}
public override void Quit()
{
MainActivity.Current?.FinishAndRemoveTask();
new Handler(Looper.MainLooper).PostDelayed(() => Java.Lang.JavaSystem.Exit(0), 400);
}
public override IDisposable AllowMulticast()
{
if (Context.GetSystemService(Context.WifiService) is not WifiManager wifi) return base.AllowMulticast();
var multicast = wifi.CreateMulticastLock("meweb");
multicast.SetReferenceCounted(false);
multicast.Acquire();
return new Held(multicast);
}
public override LocalSnapshot ReadNetwork()
{
var snapshot = new LocalSnapshot();
if (Context.GetSystemService(Context.ConnectivityService) is not ConnectivityManager connectivity) return base.ReadNetwork();
var network = connectivity.ActiveNetwork;
if (network is null) return snapshot;
var link = connectivity.GetLinkProperties(network);
var capabilities = connectivity.GetNetworkCapabilities(network);
if (link is null) return base.ReadNetwork();
var item = new LocalInterface
{
Name = link.InterfaceName,
Kind = capabilities switch
{
not null when capabilities.HasTransport(TransportType.Wifi) => LinkKind.Wifi,
not null when capabilities.HasTransport(TransportType.Cellular) => LinkKind.Mobile,
not null when capabilities.HasTransport(TransportType.Ethernet) => LinkKind.Cable,
_ => LinkKind.Unknown,
},
};
foreach (var address in link.LinkAddresses)
{
var text = Usable(address.Address?.HostAddress);
if (text is not null) item.Addresses.Add(text);
}
foreach (var route in link.Routes)
{
var gateway = route.Gateway?.HostAddress;
if (!route.IsDefaultRoute || gateway is null || gateway is "0.0.0.0" or "::") continue;
if (!item.Gateways.Contains(gateway)) item.Gateways.Add(gateway);
}
item.Gateways = item.Gateways.OrderBy(g => g.Contains(':') ? 1 : 0).ToList();
if (item.Kind == LinkKind.Wifi) item.SpeedMbps = WifiSpeed(capabilities);
snapshot.Interfaces.Add(item);
snapshot.Dns = link.DnsServers.Select(d => d.HostAddress).Where(d => d is not null).Distinct().ToList();
return snapshot;
}
static string Usable(string text)
{
var address = Addresses.Parse(text?.Split('%')[0]);
if (address is null) return null;
return Addresses.Classify(address) is AddressKind.Loopback or AddressKind.LinkLocal or AddressKind.Unspecified ? null : address.ToString();
}
static long WifiSpeed(NetworkCapabilities capabilities)
{
if (!OperatingSystem.IsAndroidVersionAtLeast(29))
return Context.GetSystemService(Context.WifiService) is WifiManager wifi ? Math.Max(0, wifi.ConnectionInfo?.LinkSpeed ?? 0) : 0;
return capabilities?.TransportInfo is WifiInfo info ? Math.Max(0, info.LinkSpeed) : 0;
}
sealed class Held(WifiManager.MulticastLock multicast) : IDisposable
{
public void Dispose()
{
if (multicast.IsHeld) multicast.Release();
}
}
}