using DodoSSH.Client.Terminal;
namespace DodoSSH.Client.Android.Platform;
///
/// Keeps in step with what is actually running.
///
///
///
/// The service is started and stopped from one place, and that place is a count rather than a lifecycle.
/// Anything else drifts: a service started when a shell opens and stopped when a tab closes would leave
/// the notification up after the last shell died on its own, and a phone showing "1 shell connected" over
/// nothing is the same dishonesty the unlock screen's shell count exists to avoid.
///
///
/// is deliberately the source of truth rather than a
/// tally kept here. It already knows that a session whose shell exited half an hour ago is not live, which
/// a counter incremented on open and decremented on close would not.
///
///
/// Three facts feed , not one: live shells, moving
/// transfers, and an idle-but-connected Files session. The last of those used to be missing entirely —
/// a shell survives backgrounding because somebody opened it, but a Files connection with nothing moving
/// looked, to this class, exactly like nothing being open at all. holdsFileSession below is that
/// gap closed, read the same way the other two facts are: asked, not cached.
///
///
/// Listening port forwards are a fourth fact, and a different kind: they only change what the notification
/// says. A forward runs over a shell's connection and stops with it, so there is never a listening forward
/// without a live shell already holding the service up — and a count that could start the service on its own
/// would only ever be wrong, in the moment a forward's Stopped had not been heard yet. What they add is the
/// receipt: the user is entitled to know the phone is holding ports open, as well as shells.
///
///
internal sealed class SessionKeepAlive : IDisposable
{
private readonly TerminalWorkspace workspace;
private readonly Func activeTransfers;
private readonly Func holdsFileSession;
private readonly Func listeningForwards;
/// The live shells.
///
/// How many transfers are moving bytes. A delegate rather than a queue, because ownership of the
/// transfer queue stays with TransfersViewModel — this class only ever asks it a question.
///
///
/// Whether the Files screen holds a live SFTP connection with nothing moving on it — the idle-but-
/// connected case a transfer count alone would miss. See TransfersViewModel.HasLiveFileSession.
///
///
/// How many port forwards are listening, across every tab. For the notification's wording only; see the
/// class remarks. See MainWindowViewModel.ListeningForwardCount.
///
public SessionKeepAlive(
TerminalWorkspace workspace,
Func activeTransfers,
Func holdsFileSession,
Func listeningForwards)
{
this.workspace = workspace;
this.activeTransfers = activeTransfers;
this.holdsFileSession = holdsFileSession;
this.listeningForwards = listeningForwards;
// Raised on whatever thread the workspace announced from — a continuation of the ended run, or the
// closer's own — which is fine: starting and stopping a service is a binder call and needs no
// particular thread. Nothing here touches the interface. That the announcement waits for the run to
// actually complete, and comes for deliberate closes too, is what makes reading LiveSessionCount
// from it honest — the event's own remark carries the stuck notification that taught us both.
workspace.SessionEnded += OnSessionEnded;
}
/// Re-reads the counts and starts or stops the service to match.
///
/// Called after anything that could change any of the facts — opening a shell, closing a tab, a transfer
/// finishing, a Files connection opening or closing, a forward starting or stopping. Calling it when nothing changed is free:
/// reconciling to the state it is already in is either a redundant startForegroundService — or,
/// now, a redundant notification post — on a running service, or a stopService on a stopped one,
/// and Android treats all of those as no-ops.
///
public void Refresh() =>
SessionForegroundService.Reconcile(
workspace.LiveSessionCount, activeTransfers(), holdsFileSession(), listeningForwards());
///
public void Dispose()
{
workspace.SessionEnded -= OnSessionEnded;
// The notification goes with the composition root. Leaving it up over a process that is shutting
// down is how an SSH client acquires a reputation for a notification you cannot get rid of.
SessionForegroundService.Reconcile(0, 0, false, 0);
}
private void OnSessionEnded(object? sender, TerminalSessionEndedEventArgs e) => Refresh();
}