using DodoSSH.Client.Session;
using global::Android.Content;
using global::Android.OS;
using global::Android.Provider;
namespace DodoSSH.Client.Android.Platform;
///
/// The handful of facts about this phone that the platform-neutral layers need handed to them.
///
///
///
/// Named Phone rather than Android because Android.Runtime.AndroidEnvironment already
/// exists and this type is not it.
///
///
/// Both members below are things docs/android-port.md called out as needing to come from the head rather
/// than be branched for inside the core: the profile directory, and a device name that is not
/// Environment.MachineName.
///
///
internal static class PhoneEnvironment
{
private static Context? context;
/// Captured once, from the launcher activity, before Avalonia starts.
///
/// The application context rather than the activity's. An activity is destroyed and recreated
/// on configuration changes this head does not declare as handled, and holding one in a static field is
/// the textbook Android leak; the application context lives as long as the process, which is exactly the
/// lifetime the composition root has.
///
public static void Attach(Context activity) =>
context = activity.ApplicationContext ?? activity;
/// Where this phone keeps its profile.
///
/// filesDir — per-app, non-roaming, not user-visible, and removed when the app is uninstalled.
/// asks for a local, non-roaming directory because two machines sharing one
/// cache file corrupts the outbox; on Android that is not merely satisfied but enforced by the platform,
/// since no other app can reach this path at all.
///
public static ClientPaths Paths =>
new(Require().FilesDir?.AbsolutePath
?? throw new InvalidOperationException("Android returned no filesDir for this application."));
///
/// What this phone calls itself in connection and keychain log entries.
///
///
///
/// Environment.MachineName returns localhost on Android, which would make every log entry
/// written from a phone indistinguishable from every other — the finding recorded in
/// docs/android-port.md §7.
///
///
/// Settings.Global.DeviceName is what the user themselves typed in Settings, so it is the name
/// they will recognise in a log written by a different device. It is null on phones that have never had
/// one set, and the fallback is the marketing model rather than the board name: a person reading a log
/// knows what a Pixel 8 is and does not know what shiba is.
///
///
public static string DeviceName
{
get
{
var chosen = Settings.Global.GetString(Require().ContentResolver, "device_name");
if (!string.IsNullOrWhiteSpace(chosen))
{
return chosen;
}
var model = Build.Model;
return string.IsNullOrWhiteSpace(model) ? "Android phone" : model;
}
}
/// Where this phone keeps copies that only have to survive the thing that made them.
///
/// cacheDir — per-app like , and unlike it, reclaimable: Android deletes from
/// here when the device runs short of storage. That is the right trade for the upload staging in
/// , whose files are worthless the moment their transfer finishes, and it
/// is why the profile is not kept here. The cost is stated rather than hidden: a file reclaimed under
/// storage pressure while its upload is still running fails that upload.
///
public static string CacheDirectory =>
Require().CacheDir?.AbsolutePath
?? throw new InvalidOperationException("Android returned no cacheDir for this application.");
///
/// The activity currently on screen, or null while the app is backgrounded.
///
///
/// Set and cleared by MainActivity. Separate from because the two have
/// genuinely different lifetimes and different uses: the application context is what outlives
/// everything and is right for starting services, and this is what a window flag or a system dialogue
/// needs — neither substitutes for the other.
///
public static global::Android.App.Activity? CurrentActivity { get; set; }
/// Raised each time the launcher activity comes back to the foreground.
///
///
/// ◆ Exists because the software keyboard can go away without Avalonia being told.
/// IInputPane.StateChanged is the only signal PhoneShell has for the keyboard opening and
/// closing, and it is not raised when Android dismisses the keyboard on the way out of the app — the
/// activity is being paused, and the pane is simply gone by the time anything could have reported it.
/// So the inset that holds the interface clear of the keyboard was still applied on the way back in,
/// and the whole phone drew an entire keyboard's height above the bottom of a screen with no keyboard
/// on it. Reproduced every time: open the keyboard anywhere, press Home, return — 838 of 2340 pixels
/// dead, and staying that way until the keyboard was opened and closed again.
///
///
/// An event here rather than a call into the view, because the view is Avalonia's and this is Android's:
/// MainActivity already owns OnResume and already writes
/// from it, and a static event is the same shape of hand-off one step further. Anything else that needs
/// to re-read platform state on the way back in can subscribe without MainActivity learning what
/// it is.
///
///
/// Subscribers are responsible for unsubscribing — this is a static event and holds whatever it is
/// given for the life of the process. PhoneShell attaches and detaches with its visual tree.
///
///
public static event EventHandler? Resumed;
/// Announces that the launcher activity is foreground again.
internal static void RaiseResumed() => Resumed?.Invoke(null, EventArgs.Empty);
/// The application context, once has run.
public static Context Require() =>
context ?? throw new InvalidOperationException(
"PhoneEnvironment was read before MainActivity attached it.");
}