Skip to main content

koan_core/remote/
devices.rs

1//! The devices this one can play on, and which of them it is controlling.
2//!
3//! Two sources: the account's other devices, which the server sends down the
4//! link, and whatever answers on the local network (`remote::nearby`), which
5//! may belong to anyone. A device found both ways is one device, reached over
6//! the local network while that connection is up: it is the shorter path.
7//!
8//! The device being controlled and the account's devices as last heard of are
9//! kept on disk, so an app iOS suspended or killed opens still controlling the
10//! same device, with the list drawn at once rather than after the link is back.
11//!
12//! A device is listed while it is present: linked, or on the network. One
13//! that stops answering is not dropped on the spot. It stays as it was for
14//! one full heartbeat past when it should have been heard from, so a single
15//! missed signal changes nothing. Then, if this device has used it (controlled
16//! it, or sent it music or a command), it is listed asleep, with when it was
17//! last seen, until it is heard from again or forgotten (`forget`); one never
18//! used is gone, since an asleep device nobody here plays on is only clutter.
19//! Asleep, it can be chosen only if a push can wake it. A device heard from
20//! again at any point is back at once, forgotten or not.
21
22use std::collections::{HashMap, HashSet};
23use std::sync::OnceLock;
24use std::time::{Duration, Instant};
25
26use parking_lot::{Condvar, Mutex};
27
28use crate::config::Config;
29use crate::remote::acks;
30use crate::remote::link::{
31    self, CommandSource, LinkCommand, LinkDevice, LinkHello, LinkReport, LinkState, Local,
32};
33
34/// Another device, as the app shows it.
35#[derive(Debug, Clone, PartialEq)]
36pub struct Device {
37    pub id: String,
38    pub name: String,
39    pub platform: String,
40    /// Signed in to the same account on the same server.
41    pub account: bool,
42    /// Whose it is, for a device another account on the server shares with
43    /// this one: playback and the queue only, as on the local network.
44    pub owner: Option<String>,
45    /// Connected to over the local network now.
46    pub nearby: bool,
47    /// Reachable at once. An account device that is not is one iOS has
48    /// suspended: a command wakes it, and music reaches it as a notification.
49    pub awake: bool,
50    /// Not reachable for longer than a heartbeat. Not reachable for less, it
51    /// is neither awake nor asleep: reconnecting, as far as anyone can tell.
52    pub asleep: bool,
53    /// Can be woken from asleep: the server holds a push token for it.
54    /// An asleep device that cannot be is shown, and cannot be chosen.
55    pub wakeable: bool,
56    /// Unix seconds when it was last reachable; `None` if never, here.
57    pub last_seen: Option<i64>,
58    /// How far waking it has got, while it is being woken or once it failed.
59    pub waking: Option<Waking>,
60    /// Plays from the same library, so music can be handed between the two.
61    pub same_library: bool,
62    /// What it last reported.
63    pub state: Option<LinkState>,
64    /// When `state` was heard.
65    pub heard: Instant,
66    /// Why it cannot be reached, when found but not connected: shown rather
67    /// than leaving the device out, so the picker says what is wrong.
68    pub problem: Option<String>,
69}
70
71impl Device {
72    /// Where its playhead is now, from where it was reported to be.
73    pub fn position_ms(&self) -> u64 {
74        let Some(state) = &self.state else { return 0 };
75        if !state.playing {
76            return state.position_ms;
77        }
78        let pos = state.position_ms + self.heard.elapsed().as_millis() as u64;
79        if state.duration_ms > 0 {
80            pos.min(state.duration_ms)
81        } else {
82            pos
83        }
84    }
85}
86
87#[derive(Default)]
88struct Store {
89    linked: bool,
90    /// When the link that is up came up, in Unix seconds.
91    linked_at: Option<i64>,
92    account: Vec<(LinkDevice, Instant)>,
93    nearby: Vec<Nearby>,
94    seen: Vec<SeenNearby>,
95    target: Option<Remembered>,
96    version: u64,
97    /// When each device was last reachable, in Unix seconds.
98    live: HashMap<String, i64>,
99    /// Devices no list carries any more, kept until they are forgotten.
100    departed: Vec<Departed>,
101    /// The devices this one has used, by id: listed asleep when out of reach.
102    used: HashSet<String>,
103    /// `account` came down the link that is up now. One held from before the
104    /// link last dropped says nothing about who is reachable at this moment.
105    fresh: bool,
106    /// Devices being woken, or that did not wake, by id.
107    waking: HashMap<String, Waking>,
108    /// When each device was last heard from on the local network, in Unix
109    /// seconds: any message, and the moment it disconnected.
110    lan_heard: HashMap<String, i64>,
111    /// The wake under way, as its generation and the device: one at a time.
112    attempt: Option<(u64, String)>,
113    /// Bumped by every wake started or abandoned.
114    wake_gen: u64,
115    /// The accounts this device lets control it, as the server says.
116    shares: Vec<String>,
117    /// Why the server refused the last change to them.
118    share_error: Option<String>,
119    /// Every account on the server, to share with.
120    accounts: Vec<String>,
121    /// The last command that did not simply arrive, for the app to say so.
122    notice: Option<Notice>,
123    /// When each Apple TV was last looked for among AirPlay announcements, by
124    /// id and host: one with none, or none that matches, is not looked for
125    /// again at every hello.
126    tv_looked: HashMap<(String, String), Instant>,
127}
128
129/// A command that was queued for a device asleep, or did not reach one: what
130/// the app tells the person, since the device's state cannot show it.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct Notice {
133    /// Counts up, so the same notice twice is still news.
134    pub seq: u64,
135    /// The device's name, as listed.
136    pub device: String,
137    pub outcome: acks::AckOutcome,
138}
139
140/// The last command that did not simply arrive.
141pub fn notice() -> Option<Notice> {
142    with(|s| s.notice.clone())
143}
144
145/// What to do with a command's outcome once it is known: `None` when it was
146/// sent by a way that brings no answer.
147pub type Then = Box<dyn FnOnce(Option<acks::AckOutcome>) + Send>;
148
149/// A stage of waking a device, in the order they are tried.
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum Waking {
152    /// Knocking on an Apple TV's AirPlay service until the box answers: a
153    /// sleeping one is woken by the sleep proxy answering for it, so that the
154    /// push after this can reach koan there.
155    Tv,
156    /// Dialling it on the local network: seen there a moment ago, it may not
157    /// be suspended yet.
158    Network,
159    /// A background push sent through the server. iOS delays or drops these
160    /// as it sees fit, and never delivers one to an app swiped away.
161    Push,
162    /// A notification on the device, asking to be tapped.
163    Notification,
164    /// None of it worked; why, for the device's row.
165    Failed(String),
166}
167
168/// A device that dropped out of every list: as it was when last reachable.
169#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
170struct Departed {
171    id: String,
172    name: String,
173    platform: String,
174    account: bool,
175    same_library: bool,
176    /// Another account's, shared with this one.
177    shared: bool,
178}
179
180/// One heartbeat: a link that hears nothing for this long pings, and one that
181/// still hears nothing is gone. A device is asleep only after a full one has
182/// passed without it.
183pub const STALE_SECS: i64 = super::wire::IDLE.as_secs() as i64;
184
185/// What is kept on disk. See the module note.
186#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
187struct Saved {
188    target: Option<Remembered>,
189    account: Vec<LinkDevice>,
190    #[serde(default)]
191    nearby: Vec<SeenNearby>,
192    /// When each device was last reachable, Unix seconds: kept so a device
193    /// the server no longer lists is not taken for one seen just now.
194    #[serde(default)]
195    live: HashMap<String, i64>,
196    /// Devices no list carries any more, kept until forgotten.
197    #[serde(default)]
198    departed: Vec<Departed>,
199    /// The devices this one has used. Absent from a file written before it
200    /// was kept, when only the device being controlled counts.
201    #[serde(default)]
202    used: Option<Vec<String>>,
203}
204
205/// A device reached on the local network, and where: dialled there at once
206/// on the next run, before Bonjour has announced anything.
207#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
208pub struct SeenNearby {
209    pub id: String,
210    pub name: String,
211    pub platform: String,
212    pub addr: String,
213    /// Unix seconds.
214    pub at: i64,
215    /// An Apple TV's AirPlay announcement, knocked on to wake it.
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub tv: Option<crate::remote::airplay::Tv>,
218}
219
220/// A device not reached on the network in this long is forgotten.
221const NEARBY_KEPT: i64 = 7 * 24 * 60 * 60;
222
223/// The device being controlled, named so that it can be shown while it is
224/// out of reach.
225#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
226struct Remembered {
227    id: String,
228    name: String,
229    platform: String,
230}
231
232struct Nearby {
233    hello: LinkHello,
234    state: Option<LinkState>,
235    at: Instant,
236}
237
238static STORE: Mutex<Option<Store>> = Mutex::new(None);
239static LOCAL: OnceLock<Local> = OnceLock::new();
240
241fn with<R>(f: impl FnOnce(&mut Store) -> R) -> R {
242    let mut store = STORE.lock();
243    f(store.get_or_insert_with(Store::default))
244}
245
246/// `f` changed something the app shows.
247fn changed<R>(f: impl FnOnce(&mut Store) -> R) -> R {
248    let out = with(|s| {
249        let out = f(s);
250        s.version += 1;
251        out
252    });
253    crate::signal::engine_changed().bump();
254    replan();
255    out
256}
257
258/// Rung when the store changes, so the clock looks again at when the list
259/// next changes on its own.
260static CLOCK: (Mutex<bool>, Condvar) = (Mutex::new(false), Condvar::new());
261
262/// A device going asleep, or past its grace period, changes the list with
263/// nothing having happened. One thread sleeps until the next such moment and
264/// announces it; it is woken early whenever the store changes.
265fn replan() {
266    static STARTED: OnceLock<()> = OnceLock::new();
267    *CLOCK.0.lock() = true;
268    CLOCK.1.notify_all();
269    STARTED.get_or_init(|| {
270        let _ = std::thread::Builder::new()
271            .name("koan-devices-clock".into())
272            .spawn(clock);
273    });
274}
275
276fn clock() {
277    loop {
278        let now = chrono::Utc::now().timestamp();
279        let wait = next_change(now).map(|at| Duration::from_secs((at - now).max(0) as u64 + 1));
280        let mut rung = CLOCK.0.lock();
281        if !*rung {
282            let timed_out = match wait {
283                Some(wait) => CLOCK.1.wait_for(&mut rung, wait).timed_out(),
284                None => {
285                    CLOCK.1.wait(&mut rung);
286                    false
287                }
288            };
289            if timed_out {
290                drop(rung);
291                // Not through `changed`, which would ring the clock again.
292                with(|s| s.version += 1);
293                crate::signal::engine_changed().bump();
294                continue;
295            }
296        }
297        *rung = false;
298    }
299}
300
301impl Store {
302    fn save(&self) {
303        let saved = Saved {
304            target: self.target.clone(),
305            account: self.account.iter().map(|(d, _)| d.clone()).collect(),
306            nearby: self.seen.clone(),
307            live: self.live.clone(),
308            departed: self.departed.clone(),
309            used: Some({
310                let mut used: Vec<String> = self.used.iter().cloned().collect();
311                used.sort();
312                used
313            }),
314        };
315        if let Ok(json) = serde_json::to_string(&saved) {
316            let _ = std::fs::write(saved_path(), json);
317        }
318    }
319}
320
321fn saved_path() -> std::path::PathBuf {
322    crate::config::config_dir().join("devices.json")
323}
324
325/// The target and account devices as the last run left them.
326fn restore() {
327    let Ok(text) = std::fs::read_to_string(saved_path()) else {
328        return;
329    };
330    let Ok(saved) = serde_json::from_str::<Saved>(&text) else {
331        return;
332    };
333    let now = Instant::now();
334    let cutoff = chrono::Utc::now().timestamp() - NEARBY_KEPT;
335    changed(|s| {
336        s.target = saved.target;
337        s.seen = saved
338            .nearby
339            .into_iter()
340            .filter(|n| n.at >= cutoff)
341            .collect();
342        s.live = saved.live;
343        s.used = match saved.used {
344            Some(used) => used.into_iter().collect(),
345            None => s.target.iter().map(|t| t.id.clone()).collect(),
346        };
347        let used = &s.used;
348        s.departed = saved
349            .departed
350            .into_iter()
351            .filter(|g| used.contains(&g.id))
352            .collect();
353        // Not linked until the server says so, and doing nothing anyone
354        // knows of: what it was playing then is not what it is playing now.
355        s.account = saved
356            .account
357            .into_iter()
358            .map(|d| {
359                (
360                    LinkDevice {
361                        linked: false,
362                        state: None,
363                        ..d
364                    },
365                    now,
366                )
367            })
368            .collect();
369    });
370}
371
372/// Link to the server and open this device to the local network, serving
373/// `local` to both. Once per process.
374pub fn start(local: Local) {
375    if LOCAL.set(local.clone()).is_err() {
376        return;
377    }
378    restore();
379    link::spawn(local.clone());
380    crate::remote::nearby::start(local);
381}
382
383/// This device, once started.
384pub fn local() -> Option<&'static Local> {
385    LOCAL.get()
386}
387
388/// The account's devices stay listed while the link is down, as they were
389/// last heard of: iOS drops the link each time it suspends the app, and a
390/// phone controlling a Mac should still be when it wakes. The server sends
391/// them afresh when the link is back.
392///
393/// Nothing here lets go of the target. A device that drops out of every list
394/// (a Mac asleep, a server restarting) is still the one the person picked,
395/// and the app shows it as out of reach until it is back or another is.
396pub fn set_linked(linked: bool) {
397    crate::remote::offline::link_changed(linked);
398    changed(|s| {
399        if linked != s.linked {
400            s.linked_at = linked.then(|| chrono::Utc::now().timestamp());
401        }
402        s.linked = linked;
403        if !linked {
404            s.fresh = false;
405        }
406    });
407}
408
409/// When the link that is up came up, in Unix seconds.
410pub fn linked_since() -> Option<i64> {
411    with(|s| s.linked.then_some(s.linked_at).flatten())
412}
413
414/// The account's other devices linked to the server now, each of which can
415/// control this one through it. Devices other accounts share with this one
416/// are this account's to control, not the other way round; the accounts this
417/// one is shared with are `shares`.
418pub fn linked_controllers() -> Vec<LinkDevice> {
419    let me = this_id();
420    with(|s| {
421        s.account
422            .iter()
423            .map(|(d, _)| d)
424            .filter(|d| d.linked && d.owner.is_none() && Some(&d.id) != me.as_ref())
425            .cloned()
426            .collect()
427    })
428}
429
430/// The name `id` goes by, as the server or the network last gave it.
431pub fn name_of(id: &str) -> Option<String> {
432    with(|s| {
433        s.account
434            .iter()
435            .map(|(d, _)| (&d.id, &d.name))
436            .chain(s.seen.iter().map(|n| (&n.id, &n.name)))
437            .find(|(i, _)| *i == id)
438            .map(|(_, name)| name.clone())
439    })
440}
441
442pub fn set_account(devices: Vec<LinkDevice>) {
443    let now = Instant::now();
444    let unix = chrono::Utc::now().timestamp();
445    changed(|s| {
446        // Reachable up to this message, if the list saying so came down this
447        // same link: the hysteresis runs from here, not from whenever it was
448        // last reported. A list held across a gap in the link proves nothing.
449        if s.fresh {
450            for (old, _) in &s.account {
451                if old.linked {
452                    s.live.insert(old.id.clone(), unix);
453                }
454            }
455        }
456        for d in &devices {
457            if d.linked {
458                s.live.insert(d.id.clone(), unix);
459                s.waking.remove(&d.id);
460            } else if let Some(seen) = d.last_seen {
461                // Whichever is later: this device may have reached it over
462                // the network since.
463                let live = s.live.entry(d.id.clone()).or_insert(seen);
464                *live = (*live).max(seen);
465            }
466            s.departed.retain(|g| g.id != d.id);
467        }
468        let leaving: Vec<Departed> = s
469            .account
470            .iter()
471            .filter(|(old, _)| !devices.iter().any(|d| d.id == old.id))
472            .map(|(old, _)| Departed {
473                id: old.id.clone(),
474                name: old.name.clone(),
475                platform: old.platform.clone(),
476                account: old.owner.is_none(),
477                shared: old.owner.is_some(),
478                same_library: true,
479            })
480            .collect();
481        for d in leaving {
482            // Never seen reachable as far as this device knows: gone, rather
483            // than taken for a device seen just now. And a device another
484            // account stops sharing is gone at once, not asleep.
485            if !s.live.contains_key(&d.id) || d.shared {
486                continue;
487            }
488            s.departed.retain(|g| g.id != d.id);
489            s.departed.push(d);
490        }
491        s.account = devices.into_iter().map(|d| (d, now)).collect();
492        s.fresh = s.linked;
493        s.save();
494    });
495}
496
497/// The accounts this device is shared with, as the server last said, and
498/// why it refused the last change, if it did.
499pub fn set_shares(grantees: Vec<String>, error: Option<String>, accounts: Vec<String>) {
500    changed(|s| {
501        s.shares = grantees;
502        s.share_error = error;
503        s.accounts = accounts;
504    });
505}
506
507/// The server's accounts, to share this device with.
508pub fn accounts() -> Vec<String> {
509    with(|s| s.accounts.clone())
510}
511
512pub fn shares() -> Vec<String> {
513    with(|s| s.shares.clone())
514}
515
516pub fn share_error() -> Option<String> {
517    with(|s| s.share_error.clone())
518}
519
520/// Signed out, or signed in elsewhere: the shares were that server's.
521pub fn forget_shares() {
522    set_shares(Vec::new(), None, Vec::new());
523}
524
525/// Send `cmd` to `id` because `source` asked this device to: a hand-off.
526///
527/// The account's own request goes anywhere. Anyone else's acts as the asker,
528/// never with this account's powers, so it does not reach this account's own
529/// devices up its link: a stranger's (Playback only) goes over the network or
530/// nowhere; a sharing account's, or a trusted network device's, goes over the
531/// network or to a device that is not this account's (the asker's own,
532/// through the server).
533pub fn send_for(source: CommandSource, id: &str, cmd: LinkCommand) -> Result<(), String> {
534    send_for_then(source, id, cmd, None)
535}
536
537/// `send_for`, handing the outcome to `then` rather than to the app's notice.
538pub fn send_for_then(
539    source: CommandSource,
540    id: &str,
541    cmd: LinkCommand,
542    then: Option<Then>,
543) -> Result<(), String> {
544    match source {
545        CommandSource::Account => send_then(id, cmd, then),
546        CommandSource::Stranger => {
547            send_nearby(id, cmd)?;
548            if let Some(then) = then {
549                then(None);
550            }
551            Ok(())
552        }
553        CommandSource::Shared | CommandSource::Nearby => {
554            let (nearby, own) = with(|s| {
555                (
556                    s.nearby.iter().any(|n| n.hello.id == id),
557                    s.account
558                        .iter()
559                        .any(|(d, _)| d.id == id && d.owner.is_none()),
560                )
561            });
562            if own && !nearby {
563                return Err(format!("{id} is this account's, not the asker's"));
564            }
565            send_then(id, cmd, then)
566        }
567    }
568}
569
570/// Send `cmd` to `id` over the local network only, never up this device's
571/// link. For what a stranger asked of this device: over the network it can
572/// only reach what the stranger could have reached itself; over the link it
573/// would act as this device's account.
574pub fn send_nearby(id: &str, cmd: LinkCommand) -> Result<(), String> {
575    let nearby = with(|s| s.nearby.iter().any(|n| n.hello.id == id));
576    if nearby && cmd.allowed_nearby() && crate::remote::nearby::send(id, cmd) {
577        Ok(())
578    } else {
579        Err(format!("{id} is not on this network"))
580    }
581}
582
583/// Let the account `grantee` on this server control this device, or stop.
584/// The server answers with the list as it now stands.
585pub fn share(grantee: &str, allow: bool) -> Result<(), String> {
586    let grantee = grantee.trim();
587    if grantee.is_empty() {
588        return Err("Name an account on your server.".into());
589    }
590    if link::report(LinkReport::Share {
591        grantee: grantee.to_string(),
592        allow,
593    }) {
594        Ok(())
595    } else {
596        Err("Not connected to your server.".into())
597    }
598}
599
600/// The devices reached on this network before, to dial first.
601pub fn remembered_nearby() -> Vec<SeenNearby> {
602    with(|s| s.seen.clone())
603}
604
605pub fn nearby_hello(hello: LinkHello, addr: &str) {
606    let host = crate::remote::airplay::host_of(addr).to_string();
607    let look = changed(|s| {
608        let kept = s
609            .seen
610            .iter()
611            .find(|n| n.id == hello.id)
612            .and_then(|n| n.tv.clone());
613        let look = hello.platform == "tvos"
614            && !crate::remote::airplay::current(kept.as_ref(), &host)
615            && look_for_tv(&mut s.tv_looked, &hello.id, &host, Instant::now());
616        // A record the look replaces is dropped; with no look, the one kept
617        // stands, as one matched by name rather than host does.
618        let tv = if look { None } else { kept };
619        s.seen.retain(|n| n.id != hello.id);
620        s.seen.push(SeenNearby {
621            id: hello.id.clone(),
622            name: hello.name.clone(),
623            platform: hello.platform.clone(),
624            addr: addr.to_string(),
625            at: chrono::Utc::now().timestamp(),
626            tv,
627        });
628        s.save();
629        s.live
630            .insert(hello.id.clone(), chrono::Utc::now().timestamp());
631        s.departed.retain(|g| g.id != hello.id);
632        s.waking.remove(&hello.id);
633        s.lan_heard
634            .insert(hello.id.clone(), chrono::Utc::now().timestamp());
635        s.nearby.retain(|n| n.hello.id != hello.id);
636        let found = look.then(|| (hello.id.clone(), hello.name.clone()));
637        s.nearby.push(Nearby {
638            hello,
639            state: None,
640            at: Instant::now(),
641        });
642        found
643    });
644    if let Some((id, name)) = look {
645        record_tv(id, name, addr.to_string());
646    }
647}
648
649/// How long a look for an Apple TV's AirPlay announcement stands, found or
650/// not, before the same TV at the same host is looked for again.
651const TV_LOOK_AGAIN: Duration = Duration::from_secs(10 * 60);
652
653/// Whether to look for the AirPlay announcement of the Apple TV `id` at
654/// `host` now, noting the look if so. A look under way counts.
655fn look_for_tv(
656    looked: &mut HashMap<(String, String), Instant>,
657    id: &str,
658    host: &str,
659    now: Instant,
660) -> bool {
661    looked.retain(|_, at| now.saturating_duration_since(*at) < TV_LOOK_AGAIN);
662    let key = (id.to_string(), host.to_string());
663    if looked.contains_key(&key) {
664        return false;
665    }
666    looked.insert(key, now);
667    true
668}
669
670/// Find the AirPlay announcement of the Apple TV `id`, met at `addr`, and keep
671/// it to wake the TV by when koan there is asleep.
672fn record_tv(id: String, name: String, addr: String) {
673    let _ = std::thread::Builder::new()
674        .name("koan-airplay".into())
675        .spawn(move || {
676            let Some(tv) = crate::remote::airplay::record(&addr, &name) else {
677                log::info!("airplay: {name}: no AirPlay announcement from {addr}");
678                return;
679            };
680            log::info!(
681                "airplay: {name} is {} on {} ({})",
682                tv.name,
683                tv.host,
684                tv.mac.as_deref().unwrap_or("no MAC")
685            );
686            with(|s| {
687                if let Some(n) = s.seen.iter_mut().find(|n| n.id == id) {
688                    n.tv = Some(tv);
689                    s.save();
690                }
691            });
692        });
693}
694
695/// The Apple TV `id` is, as its AirPlay announcement had it while awake.
696fn tv_of(id: &str) -> Option<crate::remote::airplay::Tv> {
697    with(|s| {
698        s.seen
699            .iter()
700            .find(|n| n.id == id)
701            .and_then(|n| n.tv.clone())
702    })
703}
704
705pub fn nearby_state(id: &str, state: LinkState) {
706    changed(|s| {
707        if let Some(n) = s.nearby.iter_mut().find(|n| n.hello.id == id) {
708            n.state = Some(state);
709            n.at = Instant::now();
710            let now = chrono::Utc::now().timestamp();
711            s.live.insert(id.to_string(), now);
712            s.lan_heard.insert(id.to_string(), now);
713        }
714    });
715}
716
717pub fn nearby_gone(id: &str) {
718    let cfg = Config::load().unwrap_or_default();
719    let ours = link::library_fingerprint(&cfg);
720    changed(|s| {
721        let Some(at) = s.nearby.iter().position(|n| n.hello.id == id) else {
722            return;
723        };
724        let gone = s.nearby.remove(at);
725        let now = chrono::Utc::now().timestamp();
726        s.live.insert(id.to_string(), now);
727        s.lan_heard.insert(id.to_string(), now);
728        // An account device is still listed by the server; only a stranger
729        // has to be remembered here.
730        if s.account.iter().any(|(d, _)| d.id == id) {
731            return;
732        }
733        s.departed.retain(|g| g.id != id);
734        s.departed.push(Departed {
735            id: gone.hello.id.clone(),
736            name: gone.hello.name.clone(),
737            platform: gone.hello.platform.clone(),
738            account: false,
739            same_library: ours.is_some() && gone.hello.library == ours,
740            shared: false,
741        });
742        s.save();
743    });
744}
745
746/// Forget the device `id`: out of the list until it links or is heard on the
747/// network again, which brings it straight back. An account device is
748/// forgotten by the server too, push token and all, and every other device
749/// of the account drops it. A device another account shares with this one
750/// is forgotten by declining the share. Only a
751/// device out of reach is forgotten: one reachable would be back at once.
752pub fn forget(id: &str) -> Result<(), String> {
753    let device = list().into_iter().find(|d| d.id == id);
754    if device.as_ref().is_some_and(|d| d.awake) {
755        return Err("Only a device out of reach can be forgotten.".into());
756    }
757    let account = with(|s| s.account.iter().any(|(d, _)| d.id == id));
758    if account
759        && !link::report(LinkReport::Forget {
760            device: id.to_string(),
761        })
762    {
763        return Err("Not connected to your server, which remembers it too.".into());
764    }
765    forgotten(id);
766    Ok(())
767}
768
769/// This device sent `id` something: from now on it is kept, asleep, when it
770/// is out of reach.
771fn used(id: &str) {
772    with(|s| {
773        if s.used.insert(id.to_string()) {
774            s.save();
775        }
776    });
777}
778
779/// Drop `id` from everything this device remembers of it: the server forgot
780/// it, or this device was asked to.
781pub fn forgotten(id: &str) {
782    changed(|s| {
783        s.used.remove(id);
784        s.account.retain(|(d, _)| d.id != id);
785        s.departed.retain(|g| g.id != id);
786        s.seen.retain(|n| n.id != id);
787        s.live.remove(id);
788        s.lan_heard.remove(id);
789        s.waking.remove(id);
790        if s.target.as_ref().is_some_and(|t| t.id == id) {
791            s.target = None;
792        }
793        s.save();
794    });
795    log::info!("devices: forgot {id}");
796}
797
798/// Something beside the list that the app shows with it has changed: the
799/// server's profile, whether this device is listening.
800pub fn touch() {
801    changed(|_| ());
802}
803
804/// Bumped by every change to what `list` returns, and by `touch`.
805pub fn version() -> u64 {
806    with(|s| s.version)
807}
808
809/// Whether the link to the server is up.
810pub fn linked() -> bool {
811    with(|s| s.linked)
812}
813
814/// Every other device, those playing first.
815pub fn list() -> Vec<Device> {
816    list_at(chrono::Utc::now().timestamp())
817}
818
819/// `list` as of `now`, Unix seconds.
820fn list_at(now: i64) -> Vec<Device> {
821    let cfg = Config::load().unwrap_or_default();
822    let ours = link::library_fingerprint(&cfg);
823    with(|s| {
824        let live = |id: &str| s.live.get(id).copied();
825        // Missed for less than a heartbeat.
826        let fresh = |id: &str| live(id).is_some_and(|at| now - at <= STALE_SECS);
827        // Out of reach past the heartbeat, only a device used here is listed.
828        let kept = |id: &str| fresh(id) || s.used.contains(id);
829        let mut out: Vec<Device> = s
830            .account
831            .iter()
832            .filter_map(|(d, at)| {
833                let near = s.nearby.iter().find(|n| n.hello.id == d.id);
834                let awake = d.linked || near.is_some();
835                if !awake && !kept(&d.id) {
836                    return None;
837                }
838                Some(Device {
839                    id: d.id.clone(),
840                    name: d.name.clone(),
841                    platform: d.platform.clone(),
842                    account: d.owner.is_none(),
843                    owner: d.owner.clone(),
844                    nearby: near.is_some(),
845                    awake,
846                    asleep: !awake && !fresh(&d.id),
847                    // As the server says: a push token, and a push key to
848                    // send with. One that says nothing lists only those.
849                    wakeable: d.wakeable.unwrap_or(true),
850                    last_seen: live(&d.id),
851                    waking: None,
852                    same_library: true,
853                    // The network's state is fresher, but only the server's
854                    // carries the outputs: the network is told none.
855                    state: match (near.and_then(|n| n.state.clone()), &d.state) {
856                        (Some(mut fresh), Some(linked)) => {
857                            fresh.outputs = linked.outputs.clone();
858                            Some(fresh)
859                        }
860                        (Some(fresh), None) => Some(fresh),
861                        (None, linked) => linked.clone(),
862                    },
863                    heard: near.map_or(*at, |n| n.at),
864                    problem: None,
865                })
866            })
867            .collect();
868        for n in &s.nearby {
869            if out.iter().any(|d| d.id == n.hello.id) {
870                continue;
871            }
872            out.push(Device {
873                id: n.hello.id.clone(),
874                name: n.hello.name.clone(),
875                platform: n.hello.platform.clone(),
876                account: false,
877                owner: None,
878                nearby: true,
879                awake: true,
880                asleep: false,
881                wakeable: false,
882                last_seen: live(&n.hello.id),
883                waking: None,
884                same_library: ours.is_some() && n.hello.library == ours,
885                state: n.state.clone(),
886                heard: n.at,
887                problem: None,
888            });
889        }
890        for g in &s.departed {
891            if out.iter().any(|d| d.id == g.id) || !kept(&g.id) {
892                continue;
893            }
894            let last = live(&g.id);
895            // Nothing routes to it now, whatever the label: reconnecting for
896            // a heartbeat, then asleep.
897            out.push(Device {
898                id: g.id.clone(),
899                name: g.name.clone(),
900                platform: g.platform.clone(),
901                account: g.account,
902                owner: None,
903                nearby: false,
904                awake: false,
905                asleep: !fresh(&g.id),
906                // A stranger met on this network may be woken by the server,
907                // which does so for a device behind the same router as this
908                // one; one of the account's that left the list has no token.
909                wakeable: !g.account && s.linked,
910                last_seen: last,
911                waking: None,
912                same_library: g.same_library,
913                // What it was doing is not what it is doing now.
914                state: None,
915                heard: Instant::now(),
916                problem: None,
917            });
918        }
919        // A device announced on this network but not connected is left out:
920        // it cannot be played on, and a phone that left or a stranger's app
921        // that is not discoverable would sit in the list as a reason only.
922        // The device being controlled, out of every list for now.
923        if let Some(t) = &s.target
924            && !out.iter().any(|d| d.id == t.id)
925        {
926            out.push(Device {
927                id: t.id.clone(),
928                name: t.name.clone(),
929                platform: t.platform.clone(),
930                account: false,
931                owner: None,
932                nearby: false,
933                awake: false,
934                asleep: true,
935                wakeable: false,
936                last_seen: live(&t.id),
937                waking: None,
938                same_library: true,
939                state: None,
940                heard: Instant::now(),
941                problem: Some("Out of reach".into()),
942            });
943        }
944        for d in &mut out {
945            d.waking = s.waking.get(&d.id).cloned();
946        }
947        out.sort_by_key(|d| {
948            (
949                !d.state.as_ref().is_some_and(|st| st.playing),
950                !d.awake,
951                d.name.to_lowercase(),
952            )
953        });
954        out
955    })
956}
957
958/// When `list` next changes on its own: a device crossing into asleep.
959fn next_change(now: i64) -> Option<i64> {
960    with(|s| {
961        let reachable = |id: &str| {
962            s.nearby.iter().any(|n| n.hello.id == id)
963                || s.account.iter().any(|(d, _)| d.id == id && d.linked)
964        };
965        s.live
966            .iter()
967            .filter(|(id, _)| !reachable(id))
968            .map(|(_, at)| at + STALE_SECS)
969            .filter(|t| *t > now)
970            .min()
971    })
972}
973
974/// Whether the app may choose `id`: not when nothing reaches it now and
975/// nothing can wake it, where a command would go nowhere.
976pub fn choosable(id: &str) -> Result<(), String> {
977    match list().into_iter().find(|d| d.id == id) {
978        Some(d) if !d.awake && !d.wakeable && d.problem.is_none() => Err(if d.asleep {
979            format!("{} is asleep and cannot be woken from here.", d.name)
980        } else {
981            format!("{} is reconnecting.", d.name)
982        }),
983        _ => Ok(()),
984    }
985}
986
987/// A device seen on the local network this recently is dialled there first.
988const NETWORK_RECENT: i64 = 60;
989/// How long each stage is given to bring the device in before the next.
990const NETWORK_WAIT: Duration = Duration::from_secs(3);
991const PUSH_WAIT: Duration = Duration::from_secs(6);
992const NOTIFICATION_WAIT: Duration = Duration::from_secs(45);
993/// How long an Apple TV is knocked on before the push goes anyway.
994const TV_KNOCK: Duration = Duration::from_secs(20);
995/// After the box answers, how long koan there has to be heard from on the
996/// network before the push: it may only have been asleep with the box.
997const TV_WAIT: Duration = Duration::from_secs(2);
998
999/// The stages to try, each with how long to wait on it. A device that
1000/// nothing can wake through the server is only tried on the network. An
1001/// Apple TV with an AirPlay announcement on record is woken first, so the
1002/// box is up for what follows.
1003fn wake_plan(tv: bool, seen_nearby_recently: bool, wakeable: bool) -> Vec<(Waking, Duration)> {
1004    let mut plan = Vec::new();
1005    if tv {
1006        plan.push((Waking::Tv, TV_WAIT));
1007    }
1008    if seen_nearby_recently {
1009        plan.push((Waking::Network, NETWORK_WAIT));
1010    }
1011    if wakeable {
1012        plan.push((Waking::Push, PUSH_WAIT));
1013        plan.push((Waking::Notification, NOTIFICATION_WAIT));
1014    }
1015    plan
1016}
1017
1018/// How a wake ended.
1019#[derive(Debug, Clone, PartialEq, Eq)]
1020enum Ended {
1021    Reached(Waking),
1022    Failed(String),
1023    /// Another device was chosen meanwhile: nothing more is sent.
1024    Cancelled,
1025}
1026
1027/// Walk `plan`: `act` starts each stage, `wait` blocks until the device is
1028/// reachable, the stage's time is up or the wake is abandoned, saying whether
1029/// it was reached. `current` is false once another wake has replaced this one.
1030fn run_wake(
1031    plan: &[(Waking, Duration)],
1032    current: impl Fn() -> bool,
1033    mut act: impl FnMut(&Waking) -> Result<(), String>,
1034    mut wait: impl FnMut(Duration) -> bool,
1035) -> Ended {
1036    let mut last = None;
1037    for (stage, time) in plan {
1038        if !current() {
1039            return Ended::Cancelled;
1040        }
1041        if let Err(why) = act(stage) {
1042            return Ended::Failed(why);
1043        }
1044        last = Some(stage);
1045        if wait(*time) {
1046            return Ended::Reached(stage.clone());
1047        }
1048    }
1049    if !current() {
1050        return Ended::Cancelled;
1051    }
1052    Ended::Failed(match last {
1053        Some(Waking::Notification) => {
1054            "Did not wake. A notification is waiting on it to be tapped.".into()
1055        }
1056        _ => "Did not wake.".into(),
1057    })
1058}
1059
1060fn reachable(id: &str) -> bool {
1061    with(|s| {
1062        s.nearby.iter().any(|n| n.hello.id == id)
1063            || s.account.iter().any(|(d, _)| d.id == id && d.linked)
1064    })
1065}
1066
1067/// Heard from on the local network within `NETWORK_RECENT` of `now`.
1068fn recently_on_network(id: &str, now: i64) -> bool {
1069    with(|s| {
1070        s.lan_heard
1071            .get(id)
1072            .is_some_and(|at| now - at <= NETWORK_RECENT)
1073    })
1074}
1075
1076/// Start a wake of `id`: its generation, or `None` when one of `id` is
1077/// already under way, which this then joins. Abandons any other.
1078fn begin_wake(id: &str) -> Option<u64> {
1079    with(|s| {
1080        if s.attempt.as_ref().is_some_and(|(_, d)| d == id) {
1081            return None;
1082        }
1083        s.wake_gen += 1;
1084        s.attempt = Some((s.wake_gen, id.to_string()));
1085        Some(s.wake_gen)
1086    })
1087}
1088
1089fn wake_is_current(generation: u64) -> bool {
1090    with(|s| s.attempt.as_ref().is_some_and(|(g, _)| *g == generation))
1091}
1092
1093/// Abandon the wake under way, unless it is of `keep`.
1094fn cancel_wake(keep: Option<&str>) {
1095    let cancelled = with(|s| {
1096        let other = s
1097            .attempt
1098            .as_ref()
1099            .is_some_and(|(_, d)| Some(d.as_str()) != keep);
1100        if other {
1101            let (_, id) = s.attempt.take().expect("checked");
1102            s.waking.remove(&id);
1103            s.wake_gen += 1;
1104        }
1105        other
1106    });
1107    if cancelled {
1108        // The abandoned wake's thread is waiting on this.
1109        crate::signal::engine_changed().bump();
1110    }
1111}
1112
1113/// Wake `id`, which is not reachable, in the background: on the local network
1114/// if it was heard there in the last minute, then, when the server can wake
1115/// it, a background push and then a notification on it to tap. The device's
1116/// row says which stage it is at, and why it failed if it did. Each step is
1117/// logged with its time from the start, so a wake that did not happen shows
1118/// where it stopped.
1119///
1120/// One wake at a time: choosing another device abandons this one, so nothing
1121/// more reaches the device left behind, and choosing this one again joins it.
1122/// It does not wait for the device to be labelled asleep: a phone suspended
1123/// seconds ago is the one most worth waking.
1124pub fn wake(id: &str) {
1125    let id = id.to_string();
1126    if reachable(&id) {
1127        return;
1128    }
1129    let Some(device) = list().into_iter().find(|d| d.id == id) else {
1130        return;
1131    };
1132    let now = chrono::Utc::now().timestamp();
1133    // A stranger on the network has nothing to answer a push with.
1134    let tv = (device.platform == "tvos").then(|| tv_of(&id)).flatten();
1135    let plan = wake_plan(tv.is_some(), recently_on_network(&id, now), device.wakeable);
1136    if plan.is_empty() {
1137        return;
1138    }
1139    let Some(generation) = begin_wake(&id) else {
1140        log::info!("wake: {}: already being woken", device.name);
1141        return;
1142    };
1143    let name = device.name;
1144    let _ = std::thread::Builder::new()
1145        .name("koan-wake".into())
1146        .spawn(move || {
1147            let started = Instant::now();
1148            let ms = || started.elapsed().as_millis();
1149            let current = || wake_is_current(generation);
1150            let mut box_silent = false;
1151            let ended = run_wake(
1152                &plan,
1153                current,
1154                |stage| {
1155                    log::info!("wake: {name}: {stage:?} at +{}ms", ms());
1156                    changed(|s| s.waking.insert(id.clone(), stage.clone()));
1157                    match stage {
1158                        Waking::Tv => {
1159                            if let Some(tv) = &tv {
1160                                let awake = crate::remote::airplay::wake(tv, TV_KNOCK, || {
1161                                    !current() || reachable(&id)
1162                                });
1163                                box_silent = !awake && current() && !reachable(&id);
1164                                // Awake, koan there may answer the network
1165                                // before any push.
1166                                crate::remote::nearby::dial_now();
1167                            }
1168                            Ok(())
1169                        }
1170                        Waking::Network => {
1171                            crate::remote::nearby::dial_now();
1172                            Ok(())
1173                        }
1174                        Waking::Push | Waking::Notification => {
1175                            let notify = *stage == Waking::Notification;
1176                            if link::report(LinkReport::Wake {
1177                                to: id.clone(),
1178                                notify,
1179                            }) {
1180                                Ok(())
1181                            } else {
1182                                Err("Not connected to your server, which wakes it.".into())
1183                            }
1184                        }
1185                        Waking::Failed(_) => Ok(()),
1186                    }
1187                },
1188                |time| {
1189                    let until = Instant::now() + time;
1190                    let signal = crate::signal::engine_changed();
1191                    let mut seen = signal.generation();
1192                    while !reachable(&id) {
1193                        let left = until.saturating_duration_since(Instant::now());
1194                        if left.is_zero() || !current() {
1195                            return false;
1196                        }
1197                        seen = signal.wait_until(seen, left);
1198                    }
1199                    true
1200                },
1201            );
1202            let ended = match ended {
1203                Ended::Failed(_) if box_silent => Ended::Failed(format!(
1204                    "Couldn't wake {name}: the Apple TV did not answer on this network."
1205                )),
1206                ended => ended,
1207            };
1208            match &ended {
1209                Ended::Reached(stage) => {
1210                    log::info!("wake: {name}: reached at +{}ms, after {stage:?}", ms());
1211                }
1212                Ended::Failed(why) => {
1213                    log::warn!("wake: {name}: did not wake in {}ms: {why}", ms());
1214                }
1215                Ended::Cancelled => {
1216                    log::info!("wake: {name}: abandoned at +{}ms for another device", ms());
1217                }
1218            }
1219            changed(|s| {
1220                if !s.attempt.as_ref().is_some_and(|(g, _)| *g == generation) {
1221                    return;
1222                }
1223                s.attempt = None;
1224                match ended {
1225                    Ended::Failed(why) => {
1226                        s.waking.insert(id.clone(), Waking::Failed(why));
1227                    }
1228                    _ => {
1229                        s.waking.remove(&id);
1230                    }
1231                }
1232            });
1233        });
1234}
1235
1236/// The device the app is controlling; `None` for this one.
1237pub fn target() -> Option<String> {
1238    with(|s| s.target.as_ref().map(|t| t.id.clone()))
1239}
1240
1241pub fn set_target(id: Option<String>) {
1242    cancel_wake(id.as_deref());
1243    if let Some(id) = &id {
1244        crate::remote::nearby::release(id);
1245    }
1246    let listed = id
1247        .as_ref()
1248        .and_then(|id| list().into_iter().find(|d| d.id == *id));
1249    changed(|s| {
1250        if let Some(id) = &id {
1251            s.used.insert(id.clone());
1252        }
1253        s.target = id.map(|id| match listed {
1254            Some(d) => Remembered {
1255                id,
1256                name: d.name,
1257                platform: d.platform,
1258            },
1259            None => Remembered {
1260                name: id.clone(),
1261                id,
1262                platform: String::new(),
1263            },
1264        });
1265        s.save();
1266    });
1267    // Controlling another device keeps a backgrounded app awake.
1268    crate::quiet::reapply();
1269}
1270
1271/// The app is in front again after iOS may have suspended it: link now,
1272/// prove every connection is still alive, and look at the network afresh.
1273/// What makes the other devices appear the moment the app opens rather than
1274/// when a dead socket finally times out.
1275pub fn resume() {
1276    link::nudge();
1277    crate::remote::wire::probe_all();
1278    crate::remote::nearby::refresh();
1279}
1280
1281/// Where the target's playhead is now and whether it is playing, from what
1282/// it last reported: the network's report when there is one, as `list`
1283/// prefers it. Cheap enough for every display frame, which `list` is not.
1284pub fn target_playhead() -> Option<(u64, bool)> {
1285    with(|s| {
1286        let (state, at) = reported(s, &s.target.as_ref()?.id)?;
1287        let mut position = state.position_ms;
1288        if state.playing {
1289            position += at.elapsed().as_millis() as u64;
1290            if state.duration_ms > 0 {
1291                position = position.min(state.duration_ms);
1292            }
1293        }
1294        Some((position, state.playing))
1295    })
1296}
1297
1298/// What `id` last reported and when it was heard: the network's report when
1299/// there is one, as `list` prefers it.
1300fn reported<'s>(s: &'s Store, id: &str) -> Option<(&'s LinkState, Instant)> {
1301    s.nearby
1302        .iter()
1303        .find(|n| n.hello.id == id)
1304        .and_then(|n| Some((n.state.as_ref()?, n.at)))
1305        .or_else(|| {
1306            s.account
1307                .iter()
1308                .find(|(d, _)| d.id == id)
1309                .and_then(|(d, at)| Some((d.state.as_ref()?, *at)))
1310        })
1311}
1312
1313/// The server's id for the track `id` last reported as current.
1314pub fn current_track(id: &str) -> Option<String> {
1315    with(|s| {
1316        let (state, _) = reported(s, id)?;
1317        state.queue.iter().find(|e| e.current)?.track_id.clone()
1318    })
1319}
1320
1321/// What `id` last reported, to compare with what it reports next.
1322pub fn last_report(id: &str) -> Option<LinkState> {
1323    with(|s| reported(s, id).map(|(state, _)| state.clone()))
1324}
1325
1326/// Wait up to `within` for `id` to report `track_id` as its current track
1327/// in a queue it did not have before: `before` is `last_report` from just
1328/// before asking. Sending a command only queues it: on a socket that may
1329/// have died unnoticed, in the server's store for a device asleep, or in a
1330/// push nobody has tapped. A hand-off learns the music arrived from this.
1331pub fn await_current(
1332    id: &str,
1333    track_id: &str,
1334    before: Option<&LinkState>,
1335    within: Duration,
1336) -> bool {
1337    let took = || with(|s| reported(s, id).is_some_and(|(now, _)| took(before, now, track_id)));
1338    await_until(took, within)
1339}
1340
1341/// Whether `now` shows `track_id` arrived since `before`. A hand-off replaces
1342/// the queue, so its entries are new: the current one has an id `before`'s
1343/// did not. Being on the same track already, or any other report since (a
1344/// seek, the outputs, the sleep timer), is not an answer. A device that
1345/// names no entries has only its whole report to go on.
1346fn took(before: Option<&LinkState>, now: &LinkState, track_id: &str) -> bool {
1347    let current = |state: &LinkState| state.queue.iter().find(|e| e.current).cloned();
1348    let Some(entry) = current(now) else {
1349        return false;
1350    };
1351    if entry.track_id.as_deref() != Some(track_id) {
1352        return false;
1353    }
1354    let was = before.and_then(current);
1355    match (&entry.id, was.as_ref().and_then(|e| e.id.as_ref())) {
1356        (Some(id), Some(was)) => id != was,
1357        (Some(_), None) => true,
1358        (None, _) => before != Some(now),
1359    }
1360}
1361
1362/// Wait up to `within` for `done` to hold, looking again at each change to
1363/// the engine or to this store.
1364pub fn await_until(done: impl Fn() -> bool, within: Duration) -> bool {
1365    let until = Instant::now() + within;
1366    let signal = crate::signal::engine_changed();
1367    let mut seen = signal.generation();
1368    loop {
1369        if done() {
1370            return true;
1371        }
1372        let left = until.saturating_duration_since(Instant::now());
1373        if left.is_zero() {
1374            return false;
1375        }
1376        seen = signal.wait_until(seen, left);
1377    }
1378}
1379
1380/// The target as `list` would give it.
1381pub fn target_device() -> Option<Device> {
1382    let id = target()?;
1383    list().into_iter().find(|d| d.id == id)
1384}
1385
1386/// Get `cmd` to the device `id` over a connection that is up now: the local
1387/// network's, else the link. Never queued, never a push, never the HTTP
1388/// fallback `send` has: for what is only worth saying while someone is there
1389/// to hear it, like `WatchLevels`. A stop goes both ways, since the start may
1390/// have taken either. `false` when there was no way through.
1391pub fn send_live(id: &str, cmd: LinkCommand) -> bool {
1392    let everywhere = matches!(cmd, LinkCommand::WatchLevels { on: false });
1393    let near = cmd.allowed_nearby() && crate::remote::nearby::send(id, cmd.clone());
1394    if near && !everywhere {
1395        return true;
1396    }
1397    let linked = link::report(LinkReport::Command {
1398        to: id.to_string(),
1399        command: cmd,
1400        ack: None,
1401    });
1402    near || linked
1403}
1404
1405/// Get `cmd` to the device `id`: over the local network if connected there,
1406/// else up the link, else in one request to the server, which is what a Live
1407/// Activity's button has while iOS keeps the app's link down.
1408pub fn send(id: &str, cmd: LinkCommand) -> Result<(), String> {
1409    send_then(id, cmd, None)
1410}
1411
1412/// `send`, handing the outcome to `then` once it is known, rather than to the
1413/// app's notice. Returns as soon as the command is on its way.
1414pub fn send_then(id: &str, cmd: LinkCommand, then: Option<Then>) -> Result<(), String> {
1415    choosable(id)?;
1416    used(id);
1417    let (nearby, own, shared) = with(|s| {
1418        let listed = s.account.iter().find(|(d, _)| d.id == id);
1419        (
1420            s.nearby.iter().any(|n| n.hello.id == id),
1421            listed.is_some_and(|(d, _)| d.owner.is_none()),
1422            listed.is_some_and(|(d, _)| d.owner.is_some()),
1423        )
1424    });
1425    // Another account's device, or one on the network, takes the playback
1426    // set at most, and decides for itself; what only an account may send its
1427    // own devices goes to them alone.
1428    if !own && !cmd.allowed_playback() {
1429        return Err("Only your own devices can be asked that.".into());
1430    }
1431    let routes = Routes {
1432        lan: nearby,
1433        lan_answers: nearby && answers_on_network(id),
1434        link_answers: answers_by_link(id),
1435    };
1436    // One id for the command whichever way it goes, so a device it reaches
1437    // twice acts on it once; and listened for before it is sent, so an answer
1438    // quicker than this thread is still heard.
1439    let ack = acks::next_id();
1440    let answer = acks::expect(ack, id);
1441    if routes.lan {
1442        let sent = if routes.lan_answers {
1443            crate::remote::nearby::send_acked(id, cmd.clone(), ack)
1444        } else {
1445            crate::remote::nearby::send(id, cmd.clone())
1446        };
1447        if sent {
1448            follow(id, cmd, ack, answer, Route::Network, routes, then);
1449            return Ok(());
1450        }
1451    }
1452    let account = own || shared;
1453    if link::report(LinkReport::Command {
1454        to: id.to_string(),
1455        command: cmd.clone(),
1456        ack: routes.link_answers.then_some(ack),
1457    }) {
1458        follow(id, cmd, ack, answer, Route::Link, routes, then);
1459        return Ok(());
1460    }
1461    if !account && !nearby {
1462        // Not heard of here, but perhaps by the server: the target of a Live
1463        // Activity outlives this process's list of devices.
1464        log::info!("devices: {id} is not listed; asking the server");
1465    }
1466    acks::forget(ack);
1467    let outcome = ask_server(id, &cmd, Some(ack))?;
1468    told(id, &cmd, outcome, then);
1469    Ok(())
1470}
1471
1472/// The ways a command can reach a device, and which of them bring an answer.
1473#[derive(Debug, Clone, Copy)]
1474struct Routes {
1475    lan: bool,
1476    lan_answers: bool,
1477    link_answers: bool,
1478}
1479
1480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1481enum Route {
1482    Network,
1483    Link,
1484}
1485
1486/// How long to wait for an answer by each way before trying the next. Up the
1487/// link it is the server's round trip and its own fallback to a push.
1488const NETWORK_ANSWER: Duration = Duration::from_secs(1);
1489const LINK_ANSWER: Duration = Duration::from_secs(3);
1490
1491/// Wait for `id`'s answer to the command sent under `ack` by `route`, off the
1492/// caller's thread, and try the next way when none comes. A way that brings
1493/// no answer is taken as sent, as it always was.
1494fn follow(
1495    id: &str,
1496    cmd: LinkCommand,
1497    ack: u64,
1498    answer: crossbeam_channel::Receiver<acks::AckOutcome>,
1499    route: Route,
1500    routes: Routes,
1501    then: Option<Then>,
1502) {
1503    let answers = match route {
1504        Route::Network => routes.lan_answers,
1505        Route::Link => routes.link_answers,
1506    };
1507    if !answers {
1508        acks::forget(ack);
1509        told(id, &cmd, None, then);
1510        return;
1511    }
1512    let id = id.to_owned();
1513    let _ = std::thread::Builder::new()
1514        .name("koan-ack".into())
1515        .spawn(move || {
1516            let walked = walk(&id, &cmd, ack, &answer, route, routes);
1517            acks::forget(ack);
1518            match walked {
1519                Ok(outcome) => told(&id, &cmd, outcome, then),
1520                Err(error) => unanswered(&id, &cmd, error, then),
1521            }
1522        });
1523}
1524
1525/// What became of a command sent under `ack`: the first answer by any way.
1526/// `Err` when this device could not ask the server either: no answer, which
1527/// says nothing of whether an earlier way delivered it.
1528fn walk(
1529    id: &str,
1530    cmd: &LinkCommand,
1531    ack: u64,
1532    answer: &crossbeam_channel::Receiver<acks::AckOutcome>,
1533    first: Route,
1534    routes: Routes,
1535) -> Result<Option<acks::AckOutcome>, String> {
1536    if first == Route::Network {
1537        if let Ok(outcome) = answer.recv_timeout(NETWORK_ANSWER) {
1538            return Ok(Some(outcome));
1539        }
1540        log::info!("devices: no answer from {id} on the network; trying the link");
1541        if routes.link_answers
1542            && link::report(LinkReport::Command {
1543                to: id.to_string(),
1544                command: cmd.clone(),
1545                ack: Some(ack),
1546            })
1547            && let Ok(outcome) = answer.recv_timeout(LINK_ANSWER)
1548        {
1549            return Ok(Some(outcome));
1550        }
1551    } else if let Ok(outcome) = answer.recv_timeout(LINK_ANSWER) {
1552        return Ok(Some(outcome));
1553    }
1554    log::info!("devices: no answer from {id}; asking the server");
1555    ask_server(id, cmd, Some(ack))
1556}
1557
1558/// No answer came, and this device could not ask the server: the command may
1559/// have arrived by an earlier way or may not. A sender that takes the outcome
1560/// is told none, as for a way that brings no answer, so it does not act as
1561/// if refused; the app is told the device could not be reached.
1562fn unanswered(id: &str, cmd: &LinkCommand, error: String, then: Option<Then>) {
1563    match then {
1564        Some(then) => {
1565            log::warn!("devices: no answer from {id} for {cmd:?}, and the server: {error}");
1566            then(None);
1567        }
1568        None => told(id, cmd, Some(acks::AckOutcome::Failed { error }), None),
1569    }
1570}
1571
1572/// What a command came to, once known. `None`: sent by a way that brings no
1573/// answer.
1574/// `then` has it when given; otherwise one that was queued or did not arrive
1575/// becomes the app's notice.
1576fn told(id: &str, cmd: &LinkCommand, outcome: Option<acks::AckOutcome>, then: Option<Then>) {
1577    match &outcome {
1578        Some(acks::AckOutcome::Done) | None => {}
1579        Some(acks::AckOutcome::Queued) => {
1580            log::info!("devices: {id} is asleep; {cmd:?} waits for it")
1581        }
1582        Some(other) => log::warn!("devices: {cmd:?} did not reach {id}: {other:?}"),
1583    }
1584    if let Some(then) = then {
1585        then(outcome);
1586        return;
1587    }
1588    let Some(outcome) = outcome.filter(|o| *o != acks::AckOutcome::Done) else {
1589        return;
1590    };
1591    let device = list()
1592        .into_iter()
1593        .find(|d| d.id == id)
1594        .map_or_else(|| id.to_owned(), |d| d.name);
1595    changed(|s| {
1596        let seq = s.notice.as_ref().map_or(1, |n| n.seq + 1);
1597        s.notice = Some(Notice {
1598            seq,
1599            device,
1600            outcome,
1601        });
1602    });
1603}
1604
1605/// Whether `id`, reached on the local network, answers commands.
1606fn answers_on_network(id: &str) -> bool {
1607    with(|s| s.nearby.iter().any(|n| n.hello.id == id && n.hello.acks))
1608}
1609
1610/// Whether `id`, reached up the link, answers: the server relays answers and
1611/// says the device gives them.
1612fn answers_by_link(id: &str) -> bool {
1613    crate::remote::profile::current().is_some_and(|p| p.offers(crate::remote::profile::ACK))
1614        && with(|s| s.account.iter().any(|(d, _)| d.id == id && d.acks))
1615}
1616
1617/// Get `cmd` to `id` in one request to the server, which relays it down the
1618/// device's link or wakes it. With `ack`, a server that answers waits a moment
1619/// for the device and says how it went; one that does not answers `None`.
1620fn ask_server(
1621    id: &str,
1622    cmd: &LinkCommand,
1623    ack: Option<u64>,
1624) -> Result<Option<acks::AckOutcome>, String> {
1625    let cfg = Config::load().unwrap_or_default();
1626    let client = crate::helpers::subsonic_client(&cfg).ok_or("not signed in to a server")?;
1627    let json = serde_json::to_string(cmd).map_err(|e| e.to_string())?;
1628    client
1629        .koan_command(id, &json, ack)
1630        .map_err(|e| e.to_string())
1631}
1632
1633/// `send`, by the server first: for the lock screen's buttons. iOS runs
1634/// their intent without waking the app's scene, so the link the app left
1635/// open is most likely dead, and `send` would put the command into it and
1636/// call it sent. The server knows whether the device is linked, and pushes
1637/// to it if not. A device the server cannot reach, one only on this
1638/// network, falls back to `send`.
1639pub fn send_by_server_first(id: &str, cmd: LinkCommand) -> Result<(), String> {
1640    server_first(
1641        || {
1642            ask_server(id, &cmd, Some(acks::next_id())).map(|outcome| match outcome {
1643                Some(acks::AckOutcome::Refused { reason }) => Err(reason),
1644                outcome => {
1645                    told(id, &cmd, outcome, None);
1646                    Ok(())
1647                }
1648            })?
1649        },
1650        || send(id, cmd.clone()),
1651    )
1652}
1653
1654fn server_first(
1655    server: impl FnOnce() -> Result<(), String>,
1656    otherwise: impl FnOnce() -> Result<(), String>,
1657) -> Result<(), String> {
1658    server().or_else(|why| {
1659        log::info!("devices: the server did not take it ({why}); sending it here");
1660        otherwise()
1661    })
1662}
1663
1664/// Whether `id` is listed by the server as one of the account's devices
1665/// (`Some(None)`) or one shared with it (`Some(Some(owner))`).
1666pub fn listed_owner(id: &str) -> Option<Option<String>> {
1667    with(|s| {
1668        s.account
1669            .iter()
1670            .find(|(d, _)| d.id == id)
1671            .map(|(d, _)| d.owner.clone())
1672    })
1673}
1674
1675/// This device's id, as other devices know it.
1676pub fn this_id() -> Option<String> {
1677    local().map(|l| l.identity.device_id.clone())
1678}
1679
1680#[cfg(test)]
1681pub(crate) mod tests {
1682    use super::*;
1683
1684    /// A TV dialled by address whose AirPlay record was matched by name, at
1685    /// its `.local.` host: a second hello inside the window, which does not
1686    /// look again, keeps the record to wake it by.
1687    #[test]
1688    fn a_hello_that_does_not_look_keeps_the_tv_record() {
1689        let _held = STORE_LOCK.lock();
1690        crate::config::isolate_config_for_tests();
1691        with(|s| *s = Store::default());
1692        let tv = crate::remote::airplay::Tv {
1693            name: "Living Room".into(),
1694            host: "Living-Room.local.".into(),
1695            mac: Some("AA:BB:CC:DD:EE:FF".into()),
1696            ip: Some("10.0.0.9".into()),
1697        };
1698        with(|s| {
1699            s.seen.push(SeenNearby {
1700                id: "tv".into(),
1701                name: "Living Room".into(),
1702                platform: "tvos".into(),
1703                addr: "10.0.0.9:5626".into(),
1704                at: 0,
1705                tv: Some(tv.clone()),
1706            });
1707            s.tv_looked
1708                .insert(("tv".into(), "10.0.0.9".into()), Instant::now());
1709        });
1710        let mut hello = hello("tv");
1711        hello.platform = "tvos".into();
1712        nearby_hello(hello, "10.0.0.9:5626");
1713        assert_eq!(tv_of("tv").map(|t| t.host), Some(tv.host));
1714    }
1715
1716    #[test]
1717    fn an_apple_tv_is_looked_for_once_per_host_for_a_while() {
1718        let mut looked = HashMap::new();
1719        let t0 = Instant::now();
1720        assert!(look_for_tv(&mut looked, "tv", "living-room.local.", t0));
1721        assert!(!look_for_tv(
1722            &mut looked,
1723            "tv",
1724            "living-room.local.",
1725            t0 + Duration::from_secs(5)
1726        ));
1727        // Another host, or another TV, is looked for at once.
1728        assert!(look_for_tv(
1729            &mut looked,
1730            "tv",
1731            "10.0.0.7",
1732            t0 + Duration::from_secs(5)
1733        ));
1734        assert!(look_for_tv(&mut looked, "other", "living-room.local.", t0));
1735        assert!(look_for_tv(
1736            &mut looked,
1737            "tv",
1738            "living-room.local.",
1739            t0 + TV_LOOK_AGAIN
1740        ));
1741    }
1742
1743    #[test]
1744    fn a_live_send_with_no_way_through_sends_nothing() {
1745        // No connection on the network and no link: false, and no fallback.
1746        assert!(!send_live(
1747            "nowhere-at-all",
1748            LinkCommand::WatchLevels { on: true }
1749        ));
1750    }
1751
1752    fn device(id: &str, playing: bool) -> LinkDevice {
1753        LinkDevice {
1754            id: id.into(),
1755            name: id.into(),
1756            platform: "macos".into(),
1757            linked: true,
1758            state: Some(LinkState {
1759                playing,
1760                position_ms: 1000,
1761                duration_ms: 2000,
1762                ..Default::default()
1763            }),
1764            last_seen: None,
1765            wakeable: None,
1766            owner: None,
1767            acks: false,
1768        }
1769    }
1770
1771    /// The store is process-wide: each test walks it through its states with
1772    /// this held, rather than racing the others over it.
1773    pub(crate) static STORE_LOCK: Mutex<()> = Mutex::new(());
1774
1775    fn hello(id: &str) -> LinkHello {
1776        LinkHello {
1777            id: id.into(),
1778            name: id.into(),
1779            platform: "ios".into(),
1780            library: None,
1781            acks: false,
1782            nonce: None,
1783        }
1784    }
1785
1786    #[test]
1787    fn the_lock_screen_goes_by_the_server_and_falls_back_only_when_refused() {
1788        let local = std::cell::Cell::new(0);
1789        let here = || {
1790            local.set(local.get() + 1);
1791            Ok(())
1792        };
1793        assert_eq!(server_first(|| Ok(()), here), Ok(()));
1794        assert_eq!(local.get(), 0, "the server took it: nothing sent here");
1795
1796        assert_eq!(server_first(|| Err("no such device".into()), here), Ok(()));
1797        assert_eq!(local.get(), 1, "refused there: sent here instead");
1798
1799        assert_eq!(
1800            server_first(|| Err("offline".into()), || Err("not reachable".into())),
1801            Err("not reachable".to_string())
1802        );
1803    }
1804
1805    /// A device on `track`, as the entry `entry` of its queue.
1806    fn playing(track: &str, entry: &str) -> LinkState {
1807        LinkState {
1808            queue: vec![crate::remote::link::LinkQueueEntry {
1809                id: Some(entry.into()),
1810                track_id: Some(track.into()),
1811                current: true,
1812                ..Default::default()
1813            }],
1814            ..Default::default()
1815        }
1816    }
1817
1818    /// What the apps are given for a device's playhead runs on from the
1819    /// report, so stamping it with the time the list was published does not
1820    /// set it back by however long ago the device reported.
1821    #[test]
1822    fn a_playing_devices_playhead_runs_on_from_its_report() {
1823        let _held = STORE_LOCK.lock();
1824        crate::config::isolate_config_for_tests();
1825        with(|s| *s = Store::default());
1826        nearby_hello(hello("speaker"), "10.0.0.6:5626");
1827        let at = |playing| LinkState {
1828            playing,
1829            position_ms: 1000,
1830            duration_ms: 60_000,
1831            ..Default::default()
1832        };
1833        let position = || {
1834            list()
1835                .into_iter()
1836                .find(|d| d.id == "speaker")
1837                .unwrap()
1838                .position_ms()
1839        };
1840
1841        nearby_state("speaker", at(true));
1842        std::thread::sleep(Duration::from_millis(50));
1843        assert!(position() >= 1050, "{}", position());
1844
1845        nearby_state("speaker", at(false));
1846        std::thread::sleep(Duration::from_millis(50));
1847        assert_eq!(position(), 1000, "paused, it stays put");
1848    }
1849
1850    #[test]
1851    fn a_command_queued_or_refused_becomes_the_notice_unless_its_sender_takes_it() {
1852        let _held = STORE_LOCK.lock();
1853        crate::config::isolate_config_for_tests();
1854        with(|s| *s = Store::default());
1855        nearby_hello(hello("phone"), "10.0.0.5:5626");
1856
1857        told(
1858            "phone",
1859            &LinkCommand::Pause,
1860            Some(acks::AckOutcome::Done),
1861            None,
1862        );
1863        told("phone", &LinkCommand::Pause, None, None);
1864        assert_eq!(notice(), None, "arrived, or sent as ever: nothing to say");
1865
1866        told(
1867            "phone",
1868            &LinkCommand::Pause,
1869            Some(acks::AckOutcome::Queued),
1870            None,
1871        );
1872        let queued = notice().unwrap();
1873        assert_eq!(
1874            (queued.device.as_str(), &queued.outcome),
1875            ("phone", &acks::AckOutcome::Queued)
1876        );
1877
1878        let failed = acks::AckOutcome::Failed {
1879            error: "gone".into(),
1880        };
1881        told("phone", &LinkCommand::Pause, Some(failed.clone()), None);
1882        assert_eq!(
1883            notice().unwrap().seq,
1884            queued.seq + 1,
1885            "the same twice is still news"
1886        );
1887
1888        // A sender that takes the outcome itself, as a hand-off does.
1889        let got = std::sync::Arc::new(parking_lot::Mutex::new(None));
1890        let into = got.clone();
1891        let before = notice();
1892        told(
1893            "phone",
1894            &LinkCommand::Pause,
1895            Some(failed.clone()),
1896            Some(Box::new(move |o| *into.lock() = Some(o))),
1897        );
1898        assert_eq!(*got.lock(), Some(Some(failed)));
1899        assert_eq!(notice(), before, "not said twice");
1900    }
1901
1902    /// This device failing to reach the server, after no answer by the other
1903    /// ways, is not the target refusing: the command may have arrived by an
1904    /// earlier way. A hand-off waiting on it hears no answer, so it does not
1905    /// resume the music here; the app is told the device could not be reached.
1906    #[test]
1907    fn failing_to_ask_the_server_is_no_answer_not_a_refusal() {
1908        let _held = STORE_LOCK.lock();
1909        crate::config::isolate_config_for_tests();
1910        with(|s| *s = Store::default());
1911        nearby_hello(hello("phone"), "10.0.0.5:5626");
1912
1913        let got = std::sync::Arc::new(parking_lot::Mutex::new(None));
1914        let into = got.clone();
1915        unanswered(
1916            "phone",
1917            &LinkCommand::Pause,
1918            "server unreachable".into(),
1919            Some(Box::new(move |o| *into.lock() = Some(o))),
1920        );
1921        assert_eq!(*got.lock(), Some(None), "no answer, not a refusal");
1922        assert_eq!(notice(), None);
1923
1924        unanswered(
1925            "phone",
1926            &LinkCommand::Pause,
1927            "server unreachable".into(),
1928            None,
1929        );
1930        assert!(matches!(
1931            notice().map(|n| n.outcome),
1932            Some(acks::AckOutcome::Failed { .. })
1933        ));
1934    }
1935
1936    #[test]
1937    fn a_hand_off_is_taken_only_when_the_device_reports_the_track() {
1938        let _held = STORE_LOCK.lock();
1939        crate::config::isolate_config_for_tests();
1940        with(|s| *s = Store::default());
1941        nearby_hello(hello("phone"), "10.0.0.5:5626");
1942        nearby_state("phone", playing("before", "e1"));
1943        let short = Duration::from_millis(50);
1944
1945        // Sent, and nothing heard back: queued is not taken.
1946        let before = last_report("phone");
1947        assert!(!await_current("phone", "handed", before.as_ref(), short));
1948
1949        // Already on the handed track, and then any other report from that
1950        // same queue entry (a seek, say), is no answer.
1951        nearby_state("phone", playing("handed", "e2"));
1952        let before = last_report("phone");
1953        let mut seeked = playing("handed", "e2");
1954        seeked.position_ms = 5000;
1955        nearby_state("phone", seeked);
1956        assert!(!await_current("phone", "handed", before.as_ref(), short));
1957
1958        // A new queue on another track is not this one.
1959        let reporter = std::thread::spawn(|| {
1960            std::thread::sleep(Duration::from_millis(20));
1961            nearby_state("phone", playing("other", "e3"));
1962        });
1963        assert!(!await_current(
1964            "phone",
1965            "handed",
1966            before.as_ref(),
1967            Duration::from_millis(200)
1968        ));
1969        reporter.join().unwrap();
1970
1971        // The device saying it has the track in the queue it was sent.
1972        let before = last_report("phone");
1973        let reporter = std::thread::spawn(|| {
1974            std::thread::sleep(Duration::from_millis(20));
1975            nearby_state("phone", playing("handed", "e4"));
1976        });
1977        assert!(await_current(
1978            "phone",
1979            "handed",
1980            before.as_ref(),
1981            Duration::from_secs(5)
1982        ));
1983        reporter.join().unwrap();
1984    }
1985
1986    #[test]
1987    fn a_device_naming_no_entries_is_judged_on_its_whole_report() {
1988        let unnamed = |track: &str, position_ms| LinkState {
1989            position_ms,
1990            queue: vec![crate::remote::link::LinkQueueEntry {
1991                track_id: Some(track.into()),
1992                current: true,
1993                ..Default::default()
1994            }],
1995            ..Default::default()
1996        };
1997        let before = unnamed("handed", 0);
1998        assert!(!took(Some(&before), &before, "handed"));
1999        assert!(took(Some(&before), &unnamed("handed", 10), "handed"));
2000        assert!(took(None, &unnamed("handed", 0), "handed"));
2001    }
2002
2003    #[test]
2004    fn a_device_that_stops_answering_sleeps_after_a_heartbeat_and_stays() {
2005        let _held = STORE_LOCK.lock();
2006        crate::config::isolate_config_for_tests();
2007        with(|s| *s = Store::default());
2008        let week = 7 * 24 * 60 * 60;
2009
2010        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2011        used("stranger");
2012        nearby_gone("stranger");
2013        let now = with(|s| s.live["stranger"]);
2014        let at = |t: i64| list_at(t).into_iter().find(|d| d.id == "stranger");
2015
2016        let missed_one = at(now + STALE_SECS - 1).expect("still listed");
2017        assert!(
2018            !missed_one.asleep,
2019            "one missed heartbeat does not make it asleep"
2020        );
2021        assert!(
2022            !missed_one.awake && !missed_one.nearby,
2023            "nor does it pretend to be reachable"
2024        );
2025        let asleep = at(now + STALE_SECS + 1).expect("listed asleep");
2026        assert!(asleep.asleep && !asleep.awake && !asleep.wakeable);
2027        assert_eq!(asleep.last_seen, Some(now));
2028        assert!(asleep.state.is_none(), "what it was doing is not news");
2029        assert!(
2030            at(now + week).is_some_and(|d| d.asleep),
2031            "kept long past the old grace, until forgotten"
2032        );
2033        assert_eq!(
2034            next_change(now),
2035            Some(now + STALE_SECS),
2036            "the clock wakes for the next crossing"
2037        );
2038        assert_eq!(next_change(now + STALE_SECS), None, "and then for nothing");
2039
2040        // Heard from again: back at once, with no wait.
2041        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2042        let back = at(now + STALE_SECS + 5).unwrap();
2043        assert!(back.awake && back.nearby && !back.asleep);
2044
2045        // Asleep, a stranger cannot be chosen or sent to.
2046        nearby_gone("stranger");
2047        with(|s| *s.live.get_mut("stranger").unwrap() -= STALE_SECS + 10);
2048        assert!(choosable("stranger").is_err());
2049        assert!(send("stranger", LinkCommand::Pause).is_err());
2050
2051        // An account device a push can wake, asleep for days.
2052        let mut phone = device("phone", false);
2053        phone.linked = false;
2054        phone.last_seen = Some(now - week);
2055        phone.wakeable = Some(true);
2056        set_account(vec![phone]);
2057        used("phone");
2058        let phone = list_at(now).into_iter().find(|d| d.id == "phone").unwrap();
2059        assert!(phone.asleep && phone.wakeable);
2060        assert_eq!(phone.last_seen, Some(now - week));
2061        assert!(choosable("phone").is_ok());
2062
2063        // An account device that unlinks without a push token: reconnecting
2064        // for a heartbeat, then asleep, and kept.
2065        set_linked(true);
2066        set_account(vec![device("mac", false)]);
2067        used("mac");
2068        set_account(Vec::new());
2069        let left = with(|s| s.live["mac"]);
2070        let mac = |t: i64| list_at(t).into_iter().find(|d| d.id == "mac");
2071        let reconnecting = mac(left + 1).unwrap();
2072        assert!(!reconnecting.awake && !reconnecting.asleep);
2073        let asleep = mac(left + STALE_SECS + 1).unwrap();
2074        assert!(asleep.asleep && !asleep.wakeable && asleep.account);
2075        assert!(mac(left + week).is_some());
2076        with(|s| *s = Store::default());
2077    }
2078
2079    /// A device seen and never used is gone once out of reach past the
2080    /// heartbeat; one used is kept asleep, across a relaunch, until forgotten.
2081    #[test]
2082    fn only_a_device_used_here_is_kept_asleep() {
2083        let _held = STORE_LOCK.lock();
2084        crate::config::isolate_config_for_tests();
2085        with(|s| *s = Store::default());
2086
2087        nearby_hello(hello("passing"), "10.0.0.5:5626");
2088        nearby_hello(hello("played-on"), "10.0.0.6:5626");
2089        used("played-on");
2090        nearby_gone("passing");
2091        nearby_gone("played-on");
2092        let left = with(|s| s.live["passing"]);
2093        let listed = |t: i64, id: &str| list_at(t).into_iter().any(|d| d.id == id);
2094        assert!(listed(left + 1, "passing"), "reconnecting for a heartbeat");
2095        assert!(
2096            !listed(left + STALE_SECS + 1, "passing"),
2097            "never used: gone"
2098        );
2099        assert!(listed(left + STALE_SECS + 1, "played-on"), "used: asleep");
2100
2101        // An account device the server still lists, asleep, never used here.
2102        let mut phone = device("old-phone", false);
2103        phone.linked = false;
2104        phone.last_seen = Some(left - 3600);
2105        set_account(vec![phone]);
2106        assert!(!listed(left, "old-phone"));
2107
2108        with(|s| *s = Store::default());
2109        restore();
2110        let later = left + STALE_SECS + 1;
2111        assert!(
2112            list_at(later)
2113                .iter()
2114                .any(|d| d.id == "played-on" && d.asleep),
2115            "kept across a relaunch"
2116        );
2117        assert!(!listed(later, "passing"));
2118
2119        forget("played-on").unwrap();
2120        assert!(!list().iter().any(|d| d.id == "played-on"));
2121        with(|s| *s = Store::default());
2122        restore();
2123        assert!(
2124            !list().iter().any(|d| d.id == "played-on"),
2125            "forgotten on disk"
2126        );
2127        with(|s| *s = Store::default());
2128    }
2129
2130    /// The device being controlled is used, whatever else is not; a file from
2131    /// before this was kept counts only that one, so old clutter clears.
2132    #[test]
2133    fn the_controlled_device_is_kept_and_an_old_file_keeps_only_it() {
2134        let _held = STORE_LOCK.lock();
2135        crate::config::isolate_config_for_tests();
2136        with(|s| *s = Store::default());
2137        let gone = |id: &str| Departed {
2138            id: id.into(),
2139            name: id.into(),
2140            platform: "ios".into(),
2141            account: false,
2142            same_library: false,
2143            shared: false,
2144        };
2145        let old = serde_json::json!({
2146            "target": {"id": "tv", "name": "tv", "platform": "tvos"},
2147            "account": [],
2148            "live": {"tv": 1, "sim": 1},
2149            "departed": [gone("tv"), gone("sim")],
2150        });
2151        std::fs::write(saved_path(), old.to_string()).unwrap();
2152        restore();
2153        let ids: Vec<String> = list().into_iter().map(|d| d.id).collect();
2154        assert_eq!(
2155            ids,
2156            ["tv"],
2157            "the target stays; the simulator nobody used goes"
2158        );
2159
2160        set_target(None);
2161        nearby_hello(hello("mac"), "10.0.0.7:5626");
2162        set_target(Some("mac".into()));
2163        set_target(None);
2164        nearby_gone("mac");
2165        with(|s| *s.live.get_mut("mac").unwrap() -= STALE_SECS + 10);
2166        assert!(
2167            list().iter().any(|d| d.id == "mac" && d.asleep),
2168            "controlled once: kept"
2169        );
2170        with(|s| *s = Store::default());
2171    }
2172
2173    /// Forgotten here, a stranger is gone, and kept gone across a relaunch;
2174    /// heard on the network again, it is back. Forgetting is not a block.
2175    #[test]
2176    fn a_forgotten_device_is_gone_until_it_comes_back() {
2177        let _held = STORE_LOCK.lock();
2178        crate::config::isolate_config_for_tests();
2179        with(|s| *s = Store::default());
2180        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2181        assert!(forget("stranger").is_err(), "not while it is reachable");
2182        used("stranger");
2183        nearby_gone("stranger");
2184        // Kept across a relaunch, asleep, until forgotten.
2185        with(|s| *s = Store::default());
2186        restore();
2187        assert!(list().iter().any(|d| d.id == "stranger"));
2188        forget("stranger").unwrap();
2189        assert!(!list().iter().any(|d| d.id == "stranger"));
2190        assert!(remembered_nearby().iter().all(|n| n.id != "stranger"));
2191        with(|s| *s = Store::default());
2192        restore();
2193        assert!(
2194            !list().iter().any(|d| d.id == "stranger"),
2195            "forgotten on disk too"
2196        );
2197        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2198        assert!(list().iter().any(|d| d.id == "stranger" && d.awake));
2199        with(|s| *s = Store::default());
2200    }
2201
2202    /// One of the account's is forgotten through the server, which has to be
2203    /// reachable to hear it; what the server says it forgot goes from here.
2204    #[test]
2205    fn an_account_device_is_forgotten_through_the_server() {
2206        let _held = STORE_LOCK.lock();
2207        crate::config::isolate_config_for_tests();
2208        with(|s| *s = Store::default());
2209        let mut phone = device("phone", false);
2210        phone.linked = false;
2211        phone.wakeable = Some(true);
2212        phone.last_seen = Some(chrono::Utc::now().timestamp() - 3600);
2213        set_account(vec![phone]);
2214        used("phone");
2215        assert!(
2216            forget("phone").is_err(),
2217            "no link here: the server would still list it and wake it"
2218        );
2219        assert!(list().iter().any(|d| d.id == "phone"));
2220        // What every linked device of the account hears once it is.
2221        forgotten("phone");
2222        assert!(!list().iter().any(|d| d.id == "phone"));
2223        with(|s| *s = Store::default());
2224    }
2225
2226    /// Run a wake with a device that answers in stage `answers_in`, if any:
2227    /// the stages started, and how it ended.
2228    fn walk(recent: bool, answers_in: Option<Waking>) -> (Vec<Waking>, Ended) {
2229        let plan = wake_plan(false, recent, true);
2230        let mut started = Vec::new();
2231        let current = std::cell::RefCell::new(None);
2232        let ended = run_wake(
2233            &plan,
2234            || true,
2235            |stage| {
2236                started.push(stage.clone());
2237                *current.borrow_mut() = Some(stage.clone());
2238                Ok(())
2239            },
2240            |_| *current.borrow() == answers_in,
2241        );
2242        (started, ended)
2243    }
2244
2245    #[test]
2246    fn waking_tries_the_network_first_only_when_it_was_there_a_moment_ago() {
2247        let (tried, _) = walk(true, None);
2248        assert_eq!(tried, [Waking::Network, Waking::Push, Waking::Notification]);
2249        let (tried, _) = walk(false, None);
2250        assert_eq!(tried, [Waking::Push, Waking::Notification]);
2251        assert_eq!(
2252            wake_plan(false, true, false)
2253                .iter()
2254                .map(|(w, _)| w.clone())
2255                .collect::<Vec<_>>(),
2256            [Waking::Network],
2257            "a stranger is only ever tried on the network"
2258        );
2259        assert!(wake_plan(false, false, false).is_empty());
2260    }
2261
2262    #[test]
2263    fn an_apple_tv_on_record_is_woken_before_the_push() {
2264        let stages =
2265            |plan: Vec<(Waking, Duration)>| plan.into_iter().map(|(w, _)| w).collect::<Vec<_>>();
2266        assert_eq!(
2267            stages(wake_plan(true, false, true)),
2268            [Waking::Tv, Waking::Push, Waking::Notification]
2269        );
2270        assert_eq!(
2271            stages(wake_plan(true, true, true)),
2272            [
2273                Waking::Tv,
2274                Waking::Network,
2275                Waking::Push,
2276                Waking::Notification
2277            ]
2278        );
2279    }
2280
2281    #[test]
2282    fn an_apple_tv_met_on_the_network_keeps_its_airplay_record() {
2283        let _held = STORE_LOCK.lock();
2284        crate::config::isolate_config_for_tests();
2285        with(|s| *s = Store::default());
2286        // Named so that no TV on the network running the tests matches it.
2287        let tv = crate::remote::airplay::Tv {
2288            name: "koan test TV".into(),
2289            host: "koan-test-tv.local.".into(),
2290            mac: Some("AA:BB:CC:DD:EE:FF".into()),
2291            ip: Some("10.0.0.4".into()),
2292        };
2293        let hello = |id: &str| LinkHello {
2294            name: "koan test TV".into(),
2295            platform: "tvos".into(),
2296            ..hello(id)
2297        };
2298        nearby_hello(hello("tv"), "koan-test-tv.local:50000");
2299        with(|s| s.seen[0].tv = Some(tv.clone()));
2300        // Met again on the same host, on another port: the record stands.
2301        nearby_hello(hello("tv"), "KOAN-test-tv.local:50001");
2302        assert_eq!(tv_of("tv"), Some(tv));
2303        // On another host, it is not that TV's any more.
2304        nearby_hello(hello("tv"), "koan-test-other.local:50001");
2305        assert_eq!(tv_of("tv"), None);
2306        with(|s| *s = Store::default());
2307    }
2308
2309    #[test]
2310    fn a_wake_stops_at_the_stage_that_reached_it() {
2311        let (tried, ended) = walk(true, Some(Waking::Network));
2312        assert_eq!(tried, [Waking::Network], "no push for a phone still awake");
2313        assert_eq!(ended, Ended::Reached(Waking::Network));
2314        let (tried, ended) = walk(false, Some(Waking::Push));
2315        assert_eq!(
2316            tried,
2317            [Waking::Push],
2318            "no notification once the push worked"
2319        );
2320        assert_eq!(ended, Ended::Reached(Waking::Push));
2321    }
2322
2323    #[test]
2324    fn a_push_that_does_not_wake_it_falls_back_to_a_notification_then_says_so() {
2325        let (tried, ended) = walk(false, None);
2326        assert_eq!(tried.last(), Some(&Waking::Notification));
2327        assert!(matches!(ended, Ended::Failed(why) if why.contains("notification")));
2328        let plan = wake_plan(false, false, true);
2329        assert!(plan[0].1 <= Duration::from_secs(8) && plan[0].1 >= Duration::from_secs(5));
2330    }
2331
2332    #[test]
2333    fn a_stage_that_cannot_start_ends_the_wake_with_why() {
2334        let ended = run_wake(
2335            &wake_plan(false, false, true),
2336            || true,
2337            |_| Err("Not connected".into()),
2338            |_| true,
2339        );
2340        assert_eq!(ended, Ended::Failed("Not connected".into()));
2341    }
2342
2343    /// Choosing another device mid-wake sends nothing more to the one left
2344    /// behind: no notification "wants to play here" for a choice taken back.
2345    #[test]
2346    fn a_wake_abandoned_for_another_choice_sends_nothing_more() {
2347        let live = std::cell::Cell::new(true);
2348        let mut tried = Vec::new();
2349        let ended = run_wake(
2350            &wake_plan(false, true, true),
2351            || live.get(),
2352            |stage| {
2353                tried.push(stage.clone());
2354                Ok(())
2355            },
2356            |_| {
2357                live.set(false);
2358                false
2359            },
2360        );
2361        assert_eq!(tried, [Waking::Network]);
2362        assert_eq!(ended, Ended::Cancelled);
2363    }
2364
2365    #[test]
2366    fn one_wake_at_a_time_joined_by_the_same_choice_and_ended_by_another() {
2367        let _held = STORE_LOCK.lock();
2368        with(|s| *s = Store::default());
2369        let first = begin_wake("phone").expect("starts");
2370        assert!(begin_wake("phone").is_none(), "choosing it again joins it");
2371        assert!(wake_is_current(first));
2372        cancel_wake(Some("phone"));
2373        assert!(wake_is_current(first), "the same choice does not end it");
2374        cancel_wake(Some("mac"));
2375        assert!(!wake_is_current(first), "another choice does");
2376        let second = begin_wake("mac").unwrap();
2377        assert!(begin_wake("phone").is_some() && !wake_is_current(second));
2378        with(|s| *s = Store::default());
2379    }
2380
2381    /// The wake starts the moment a device stops being reachable, not once
2382    /// the heartbeat has passed and it is labelled asleep.
2383    #[test]
2384    fn a_device_chosen_seconds_after_it_dropped_is_woken() {
2385        let _held = STORE_LOCK.lock();
2386        crate::config::isolate_config_for_tests();
2387        with(|s| *s = Store::default());
2388        set_linked(true);
2389        set_account(vec![device("phone", false)]);
2390        let mut phone = device("phone", false);
2391        phone.linked = false;
2392        phone.wakeable = Some(true);
2393        phone.last_seen = Some(chrono::Utc::now().timestamp() - 10);
2394        set_account(vec![phone]);
2395        let listed = list().into_iter().find(|d| d.id == "phone").unwrap();
2396        assert!(!listed.asleep && !listed.awake, "reconnecting, not asleep");
2397        wake("phone");
2398        // Its thread may already have finished: with no link here, the push
2399        // stage fails at once and says so on the row.
2400        let ran = (0..100).any(|_| {
2401            let seen = with(|s| {
2402                s.attempt.as_ref().is_some_and(|(_, d)| d == "phone")
2403                    || s.waking.contains_key("phone")
2404            });
2405            if !seen {
2406                std::thread::sleep(Duration::from_millis(10));
2407            }
2408            seen
2409        });
2410        assert!(ran, "the pipeline runs");
2411        cancel_wake(None);
2412        with(|s| *s = Store::default());
2413    }
2414
2415    /// A device linked for ten minutes that left half a minute ago is tried
2416    /// on the network first: the time is when it was last heard there, not
2417    /// when it first connected.
2418    #[test]
2419    fn last_heard_on_the_network_is_the_last_message_not_the_first() {
2420        let _held = STORE_LOCK.lock();
2421        crate::config::isolate_config_for_tests();
2422        with(|s| *s = Store::default());
2423        nearby_hello(hello("phone"), "10.0.0.7:5626");
2424        with(|s| {
2425            for seen in &mut s.seen {
2426                seen.at -= 10 * 60;
2427            }
2428        });
2429        nearby_state("phone", LinkState::default());
2430        nearby_gone("phone");
2431        let left = with(|s| s.lan_heard["phone"]);
2432        assert!(recently_on_network("phone", left + 30));
2433        assert!(!recently_on_network("phone", left + NETWORK_RECENT + 1));
2434        with(|s| *s = Store::default());
2435    }
2436
2437    /// A device met only on the network, asleep, is woken through the
2438    /// server, which decides whether it may by where the two devices are; with
2439    /// no server to ask, it is not woken at all.
2440    #[test]
2441    fn a_stranger_asleep_on_the_network_is_woken_through_the_server_if_linked() {
2442        let _held = STORE_LOCK.lock();
2443        crate::config::isolate_config_for_tests();
2444        with(|s| *s = Store::default());
2445        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2446        used("stranger");
2447        nearby_gone("stranger");
2448        with(|s| {
2449            *s.live.get_mut("stranger").unwrap() -= 10 * 60;
2450            *s.lan_heard.get_mut("stranger").unwrap() -= 10 * 60;
2451        });
2452        let listed = list().into_iter().find(|d| d.id == "stranger").unwrap();
2453        assert!(listed.asleep && !listed.wakeable, "no server to ask");
2454        wake("stranger");
2455        assert!(
2456            with(|s| s.attempt.is_none() && s.waking.is_empty()),
2457            "too long gone for the network, and no server"
2458        );
2459
2460        set_linked(true);
2461        let listed = list().into_iter().find(|d| d.id == "stranger").unwrap();
2462        assert!(listed.asleep && listed.wakeable);
2463        assert!(choosable("stranger").is_ok());
2464        let plan = wake_plan(false, false, listed.wakeable);
2465        assert_eq!(plan[0].0, Waking::Push, "asks the server");
2466        with(|s| *s = Store::default());
2467    }
2468
2469    /// What a stranger or a sharing account asks this device to pass on goes
2470    /// over the network or nowhere: never up this device's link, as its
2471    /// account, to devices the asker has no claim on.
2472    #[test]
2473    fn a_hand_off_asked_by_a_stranger_never_goes_up_the_link() {
2474        let _held = STORE_LOCK.lock();
2475        crate::config::isolate_config_for_tests();
2476        with(|s| *s = Store::default());
2477        set_linked(true);
2478        set_account(vec![device("mac", false)]);
2479        let play = LinkCommand::Play {
2480            track_ids: vec!["t".into()],
2481            start_at: 0,
2482            position_ms: 0,
2483            paused: false,
2484            handoff: true,
2485            mode: crate::player::state::QueueMode::Keep,
2486        };
2487        assert!(
2488            send_nearby("mac", play).is_err(),
2489            "the account's Mac is reached only through the link"
2490        );
2491        with(|s| *s = Store::default());
2492    }
2493
2494    /// A hand-off asked by anyone but the account acts as the asker: never up
2495    /// this device's link to the account's own devices.
2496    #[test]
2497    fn a_hand_off_for_someone_else_never_reaches_the_accounts_own_devices() {
2498        let _held = STORE_LOCK.lock();
2499        crate::config::isolate_config_for_tests();
2500        with(|s| *s = Store::default());
2501        set_linked(true);
2502        set_account(vec![device("mac", false)]);
2503        let play = || LinkCommand::Play {
2504            track_ids: vec!["t".into()],
2505            start_at: 0,
2506            position_ms: 0,
2507            paused: false,
2508            handoff: true,
2509            mode: crate::player::state::QueueMode::Keep,
2510        };
2511        for source in [
2512            CommandSource::Shared,
2513            CommandSource::Nearby,
2514            CommandSource::Stranger,
2515        ] {
2516            let refused = send_for(source, "mac", play()).unwrap_err();
2517            assert!(
2518                refused.contains("this account's") || refused.contains("not on this network"),
2519                "{source:?}: {refused}"
2520            );
2521        }
2522        with(|s| *s = Store::default());
2523    }
2524
2525    /// Another account's device that stops being shared is gone at once.
2526    #[test]
2527    fn a_device_no_longer_shared_is_dropped_at_once() {
2528        let _held = STORE_LOCK.lock();
2529        crate::config::isolate_config_for_tests();
2530        with(|s| *s = Store::default());
2531        set_linked(true);
2532        let mut theirs = device("their-phone", false);
2533        theirs.owner = Some("k".into());
2534        set_account(vec![theirs]);
2535        let listed = list().into_iter().find(|d| d.id == "their-phone").unwrap();
2536        assert_eq!(listed.owner.as_deref(), Some("k"));
2537        assert!(!listed.account, "not this account's own");
2538        assert!(send("their-phone", LinkCommand::Sync { full: false }).is_err());
2539        set_account(Vec::new());
2540        assert!(!list().iter().any(|d| d.id == "their-phone"));
2541        with(|s| *s = Store::default());
2542    }
2543
2544    /// A stranger whose connection just closed reads as reconnecting, and is
2545    /// not offered: nothing would carry a command to it.
2546    #[test]
2547    fn a_stranger_that_just_left_cannot_be_chosen() {
2548        let _held = STORE_LOCK.lock();
2549        crate::config::isolate_config_for_tests();
2550        with(|s| *s = Store::default());
2551        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2552        nearby_gone("stranger");
2553        let listed = list().into_iter().find(|d| d.id == "stranger").unwrap();
2554        assert!(!listed.awake && !listed.asleep && !listed.nearby);
2555        assert!(choosable("stranger").is_err());
2556        assert!(send("stranger", LinkCommand::Pause).is_err());
2557        with(|s| *s = Store::default());
2558    }
2559
2560    /// A list held while this app's own link was down says nothing about
2561    /// when anyone was seen: the server's `last_seen` stands.
2562    #[test]
2563    fn a_list_held_across_a_gap_in_the_link_does_not_stamp_now() {
2564        let _held = STORE_LOCK.lock();
2565        crate::config::isolate_config_for_tests();
2566        with(|s| *s = Store::default());
2567        set_linked(true);
2568        set_account(vec![device("phone", true)]);
2569        used("phone");
2570        // Suspended for hours; the phone left meanwhile.
2571        with(|s| *s.live.get_mut("phone").unwrap() -= 4 * 60 * 60);
2572        set_linked(false);
2573        let hours_ago = chrono::Utc::now().timestamp() - 3 * 60 * 60;
2574        set_linked(true);
2575        let mut phone = device("phone", false);
2576        phone.linked = false;
2577        phone.last_seen = Some(hours_ago);
2578        set_account(vec![phone]);
2579        let phone = list().into_iter().find(|d| d.id == "phone").unwrap();
2580        assert!(phone.asleep, "not reconnecting: it went hours ago");
2581        // The stamp from when it was linked is earlier than the server's, so
2582        // the server's stands.
2583        assert!(phone.last_seen.unwrap() >= hours_ago);
2584        assert!(
2585            phone.last_seen.unwrap() < chrono::Utc::now().timestamp() - STALE_SECS,
2586            "not seen just now"
2587        );
2588        with(|s| *s = Store::default());
2589    }
2590
2591    /// Relaunched, a device the server no longer lists is gone, not a device
2592    /// seen just now; and one it does list shows nothing it was doing.
2593    #[test]
2594    fn a_restored_device_the_server_drops_is_gone() {
2595        let _held = STORE_LOCK.lock();
2596        crate::config::isolate_config_for_tests();
2597        with(|s| *s = Store::default());
2598        // What an older run left: no times for these.
2599        let saved = Saved {
2600            target: None,
2601            account: vec![device("mac", true), device("phone", true)],
2602            nearby: Vec::new(),
2603            live: HashMap::new(),
2604            departed: Vec::new(),
2605            used: None,
2606        };
2607        std::fs::write(saved_path(), serde_json::to_string(&saved).unwrap()).unwrap();
2608        restore();
2609        let listed = list();
2610        assert!(listed.iter().all(|d| d.state.is_none() && !d.awake));
2611        set_linked(true);
2612        let mut phone = device("phone", false);
2613        phone.linked = false;
2614        phone.wakeable = Some(true);
2615        set_account(vec![phone]);
2616        assert!(
2617            !list().iter().any(|d| d.id == "mac"),
2618            "never seen here, and not listed: dropped"
2619        );
2620        with(|s| *s = Store::default());
2621    }
2622
2623    /// A server with no push key cannot wake anything, whatever tokens it
2624    /// holds: such a device is shown asleep and cannot be chosen.
2625    #[test]
2626    fn a_device_the_server_cannot_push_is_not_wakeable() {
2627        let _held = STORE_LOCK.lock();
2628        crate::config::isolate_config_for_tests();
2629        with(|s| *s = Store::default());
2630        let mut phone = device("phone", false);
2631        phone.linked = false;
2632        phone.wakeable = Some(false);
2633        phone.last_seen = Some(chrono::Utc::now().timestamp() - 3600);
2634        set_account(vec![phone]);
2635        used("phone");
2636        let phone = list().into_iter().find(|d| d.id == "phone").unwrap();
2637        assert!(phone.asleep && !phone.wakeable);
2638        assert!(choosable("phone").is_err());
2639        with(|s| *s = Store::default());
2640    }
2641
2642    #[test]
2643    fn a_device_found_both_ways_is_one_and_a_lost_target_is_kept() {
2644        let _held = STORE_LOCK.lock();
2645        crate::config::isolate_config_for_tests();
2646        with(|s| *s = Store::default());
2647        set_account(vec![device("mac", false), device("phone", true)]);
2648        let listed = list();
2649        assert_eq!(listed[0].id, "phone", "playing first");
2650        assert_eq!(listed.len(), 2);
2651
2652        nearby_hello(
2653            LinkHello {
2654                id: "mac".into(),
2655                name: "Mac".into(),
2656                platform: "macos".into(),
2657                library: None,
2658                acks: false,
2659                nonce: None,
2660            },
2661            "mac.local:5626",
2662        );
2663        nearby_hello(
2664            LinkHello {
2665                id: "tv".into(),
2666                name: "Living room".into(),
2667                platform: "macos".into(),
2668                library: Some("elsewhere".into()),
2669                acks: false,
2670                nonce: None,
2671            },
2672            "10.0.0.9:5626",
2673        );
2674        let listed = list();
2675        assert_eq!(listed.len(), 3);
2676        let mac = listed.iter().find(|d| d.id == "mac").unwrap();
2677        assert!(mac.account && mac.nearby && mac.same_library);
2678        let tv = listed.iter().find(|d| d.id == "tv").unwrap();
2679        assert!(!tv.account && !tv.same_library);
2680
2681        set_target(Some("tv".into()));
2682        nearby_gone("tv");
2683        // Long past the heartbeat: asleep, kept, and still the target.
2684        with(|s| *s.live.get_mut("tv").unwrap() -= STALE_SECS + 31 * 60);
2685        assert_eq!(target(), Some("tv".into()), "kept while out of reach");
2686        let tv = list().into_iter().find(|d| d.id == "tv").unwrap();
2687        assert_eq!(tv.name, "Living room");
2688        assert!(!tv.awake && tv.asleep);
2689
2690        // What a relaunch finds.
2691        with(|s| *s = Store::default());
2692        restore();
2693        assert_eq!(target(), Some("tv".into()));
2694        let listed = list();
2695        assert!(
2696            listed.iter().any(|d| d.id == "mac" && !d.awake),
2697            "last heard of, not linked"
2698        );
2699        let remembered = remembered_nearby();
2700        assert!(
2701            remembered
2702                .iter()
2703                .any(|n| n.id == "tv" && n.addr == "10.0.0.9:5626")
2704        );
2705
2706        // What a device on the network asks this one to pass on stays on the
2707        // network: the account's Mac, out of reach there, is not sent to up
2708        // the link.
2709        nearby_gone("mac");
2710        let play = LinkCommand::Play {
2711            track_ids: vec!["t".into()],
2712            start_at: 0,
2713            position_ms: 0,
2714            paused: false,
2715            handoff: true,
2716            mode: crate::player::state::QueueMode::Keep,
2717        };
2718        assert!(listed.iter().any(|d| d.id == "mac" && d.account));
2719        assert!(send_nearby("mac", play).is_err());
2720
2721        set_target(None);
2722        set_account(Vec::new());
2723        nearby_gone("mac");
2724        assert!(
2725            list().iter().all(|d| d.last_seen.is_some()),
2726            "only what has just left, kept for a heartbeat"
2727        );
2728        with(|s| *s = Store::default());
2729    }
2730}