package ui import ( "errors" "fmt" "strings" "gitea.lab.audasmedia.com.au/sam/artemis_control/internal/adb" ) // ErrLocked is returned when a secure keyguard is showing. Artemis never tries // to get past it: no PIN, pattern or password is stored, read or typed // (AGENTS.md section 13 Q2). var ErrLocked = errors.New("phone is locked; unlock it and retry") // KeyguardState reports whether the lock screen is showing and, separately, // whether the device has a secure lock (PIN, pattern or password) at all. // // The two are independent: a phone with no PIN still shows a keyguard when it // wakes, but that one may be dismissed. A secure one may not. func KeyguardState(r *adb.Runner) (showing bool, secure bool, err error) { dump, err := r.Shell("dumpsys window") if err != nil { return false, false, fmt.Errorf("read window state: %w", err) } showing, err = keyguardShowing(dump) if err != nil { return false, false, err } secure, err = userHasSecureLock(r) if err != nil { return false, false, err } return showing, secure, nil } // keyguardShowing parses `dumpsys window` for the KeyguardServiceDelegate // block. An unparsable dump is an error: guessing here would mean acting on a // phone whose lock state is unknown. func keyguardShowing(dump string) (bool, error) { showing := false sawDelegate := false sawShowing := false inDelegate := false for _, raw := range strings.Split(dump, "\n") { line := strings.TrimSpace(raw) switch { case strings.HasPrefix(line, "KeyguardServiceDelegate"): inDelegate, sawDelegate = true, true continue case strings.HasPrefix(line, "isKeyguardShowing="): if strings.TrimPrefix(line, "isKeyguardShowing=") == "true" { showing = true } continue } if !inDelegate { continue } // The delegate block is a flat list of key=value lines. if !strings.Contains(line, "=") { inDelegate = false continue } if strings.HasPrefix(line, "showing=") { showing = strings.TrimPrefix(line, "showing=") == "true" sawShowing = true } } if !sawDelegate || !sawShowing { return false, fmt.Errorf("could not read the keyguard state from dumpsys window") } return showing, nil } // userHasSecureLock asks the lock settings service whether the current user has // a secure lock screen. It never asks for or handles the credential itself. func userHasSecureLock(r *adb.Runner) (bool, error) { out, err := r.Shell("cmd lock_settings is-user-secure") if err != nil { return false, fmt.Errorf("read lock state: %w", err) } switch strings.TrimSpace(out) { case "true": return true, nil case "false": return false, nil } return false, fmt.Errorf("unexpected lock state %q from cmd lock_settings is-user-secure", strings.TrimSpace(out)) } // RequireUnlocked refuses to act while a secure keyguard is showing. // // A non-secure keyguard (the phone has no PIN at all) is dismissed, because // that screen guards nothing. A secure one is left alone: Artemis stops and // says so. func RequireUnlocked(r *adb.Runner) error { showing, secure, err := KeyguardState(r) if err != nil { return fmt.Errorf("cannot confirm the phone is unlocked: %w", err) } if !showing { return nil } if secure { return ErrLocked } if _, err := r.Shell("wm dismiss-keyguard"); err != nil { return fmt.Errorf("dismiss the non-secure keyguard: %w", err) } return nil }