mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 23:07:03 +01:00
phone_focus_mode is built for a rooted phone: the hosts layer is a read-only
bind mount over /system/etc/hosts, chosen so that even root-in-a-terminal
cannot overwrite it. None of that ports to a stock locked device. This splits
out the parts that are device-independent so one policy can drive both.
python_pkg/focus_policy parses config.sh rather than copying it, so the shell
config stays the source of truth and the two cannot drift. It rejects the
shipped REDACTED_* coordinate placeholder instead of defaulting to (0,0),
which would move "home" to the Atlantic and quietly disable the location gate.
blocklist.py converts the hosts file to domains for a DoT resolver or a local
VpnService. Three properties of the real 194k-line file make the obvious parser
wrong, and all three are handled: generate_hosts_file.sh *comments out*
deliberate unblocks rather than deleting them, so stripping '#' re-blocks
facebook/messenger/linkedin/4chan; the StevenBlack header carries loopback and
IPv6 housekeeping that would blackhole localhost; and ~1060 upstream annotation
tokens ('#[adware.zeno]', '"spam') render as address=/"spam/, which makes
dnsmasq refuse to start -- taking DNS down rather than blocking anything.
Verified with dnsmasq --test over all 185681 generated domains.
strip_workout_hosts.py had the same class of bug on the live rooted phone:
exact set membership meant the allowlisted googlevideo.com never matched the
dynamic r*---sn-*.googlevideo.com hosts YouTube actually streams from, so the
workout unblock was only partly working. Now suffix-matched (17 lines stripped,
was 14). The three new tests were checked against the original code in a
throwaway worktree; two of them fail there, so they pin the fix rather than
passing vacuously.
focus_owner is a device-owner app that deliberately does nothing yet. It ships
clearDeviceOwnerApp() implemented and tested first, because dpm
remove-active-admin does not work on a device owner: an app that locks the
device down before proving it can let go leaves a soft-brick recoverable only
through fastboot on a locked bootloader. Built, installed and launched on the
Pixel 6a; dumpsys afterwards confirms Device Owner Type -1 and no user
restrictions, i.e. the phone is unchanged.
Measured while scoping this, and not yet reflected in any enforcement code:
on Android 16 both pm suspend and pm disable-user are cleared by a reboot, so
shell-level app blocking is per-session only. setApplicationHidden is assumed
to persist under Device Owner but is untested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
227 lines
8.7 KiB
Python
227 lines
8.7 KiB
Python
"""Tests for the policy model: geometry, curfew windows, and allow decisions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import time
|
|
|
|
import pytest
|
|
|
|
from python_pkg.focus_policy.model import (
|
|
CurfewWindow,
|
|
FocusPolicy,
|
|
HomeLocation,
|
|
PolicyError,
|
|
)
|
|
|
|
HOME = HomeLocation(latitude=52.2297, longitude=21.0122)
|
|
METRES_PER_DEGREE_LAT = 111_320.0
|
|
|
|
|
|
def _policy(
|
|
*,
|
|
allowed_packages: frozenset[str] | None = None,
|
|
night_allowed_packages: frozenset[str] | None = None,
|
|
curfew: CurfewWindow | None = None,
|
|
launcher_package: str | None = None,
|
|
) -> FocusPolicy:
|
|
"""Build a small policy, overriding individual fields per test."""
|
|
return FocusPolicy(
|
|
home=HOME,
|
|
allowed_packages=(
|
|
frozenset({"com.good", "pl.mbank", "com.launcher"})
|
|
if allowed_packages is None
|
|
else allowed_packages
|
|
),
|
|
night_allowed_packages=(
|
|
frozenset({"pl.mbank"})
|
|
if night_allowed_packages is None
|
|
else night_allowed_packages
|
|
),
|
|
never_disable_prefixes=("com.android.providers", "com.android.settings"),
|
|
curfew=curfew,
|
|
launcher_package=launcher_package,
|
|
)
|
|
|
|
|
|
class TestHomeLocation:
|
|
"""Coordinate validation and distance maths."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("kwargs", "expected"),
|
|
[
|
|
({"latitude": 91.0, "longitude": 0.0}, "latitude"),
|
|
({"latitude": -91.0, "longitude": 0.0}, "latitude"),
|
|
({"latitude": 0.0, "longitude": 181.0}, "longitude"),
|
|
({"latitude": 0.0, "longitude": -181.0}, "longitude"),
|
|
({"latitude": 0.0, "longitude": 0.0, "radius_m": 0.0}, "radius_m"),
|
|
({"latitude": 0.0, "longitude": 0.0, "hysteresis_m": -1.0}, "hysteresis_m"),
|
|
],
|
|
)
|
|
def test_rejects_impossible_values(
|
|
self,
|
|
kwargs: dict[str, float],
|
|
expected: str,
|
|
) -> None:
|
|
"""Out-of-range coordinates and distances raise, naming the bad field."""
|
|
with pytest.raises(PolicyError, match=expected):
|
|
HomeLocation(**kwargs)
|
|
|
|
def test_accepts_boundary_coordinates(self) -> None:
|
|
"""The poles and the antimeridian are valid places."""
|
|
assert HomeLocation(latitude=90.0, longitude=180.0).latitude == 90.0
|
|
assert HomeLocation(latitude=-90.0, longitude=-180.0).longitude == -180.0
|
|
|
|
def test_distance_to_self_is_zero(self) -> None:
|
|
"""A point measured against itself is zero metres away."""
|
|
assert HOME.distance_m(HOME.latitude, HOME.longitude) == pytest.approx(0.0)
|
|
|
|
def test_distance_matches_known_offset(self) -> None:
|
|
"""One degree of latitude is ~111 km; used to pin the Haversine maths."""
|
|
away = HOME.distance_m(HOME.latitude + 1.0, HOME.longitude)
|
|
assert away == pytest.approx(METRES_PER_DEGREE_LAT, rel=0.01)
|
|
|
|
def test_inside_within_radius_regardless_of_state(self) -> None:
|
|
"""Well inside the radius is "home" whether or not focus is already on."""
|
|
near = HOME.latitude + 50 / METRES_PER_DEGREE_LAT
|
|
assert HOME.is_inside(near, HOME.longitude, currently_focused=False)
|
|
assert HOME.is_inside(near, HOME.longitude, currently_focused=True)
|
|
|
|
def test_hysteresis_prevents_flapping_at_the_boundary(self) -> None:
|
|
"""Between radius and radius+hysteresis the answer depends on state.
|
|
|
|
This is the whole point of hysteresis: arriving has not yet triggered
|
|
focus, but leaving has not yet released it, so GPS jitter around the
|
|
boundary cannot toggle enforcement on every poll.
|
|
"""
|
|
edge = HOME.latitude + 160 / METRES_PER_DEGREE_LAT
|
|
assert not HOME.is_inside(edge, HOME.longitude, currently_focused=False)
|
|
assert HOME.is_inside(edge, HOME.longitude, currently_focused=True)
|
|
|
|
def test_outside_hysteresis_band_is_away_in_both_states(self) -> None:
|
|
"""Beyond radius+hysteresis the user is away regardless of state."""
|
|
far = HOME.latitude + 500 / METRES_PER_DEGREE_LAT
|
|
assert not HOME.is_inside(far, HOME.longitude, currently_focused=False)
|
|
assert not HOME.is_inside(far, HOME.longitude, currently_focused=True)
|
|
|
|
|
|
class TestCurfewWindow:
|
|
"""Window containment, including the midnight wrap."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("moment", "expected"),
|
|
[
|
|
(time(22, 59), False),
|
|
(time(23, 0), True),
|
|
(time(23, 59), True),
|
|
(time(0, 0), True),
|
|
(time(4, 59), True),
|
|
(time(5, 0), False),
|
|
(time(12, 0), False),
|
|
],
|
|
)
|
|
def test_wrapping_window_spans_midnight(
|
|
self,
|
|
moment: time,
|
|
*,
|
|
expected: bool,
|
|
) -> None:
|
|
"""23:00-05:00 covers the late evening and the small hours alike."""
|
|
window = CurfewWindow(start=time(23, 0), end=time(5, 0))
|
|
assert window.contains(moment) is expected
|
|
|
|
@pytest.mark.parametrize(
|
|
("moment", "expected"),
|
|
[
|
|
(time(8, 59), False),
|
|
(time(9, 0), True),
|
|
(time(16, 59), True),
|
|
(time(17, 0), False),
|
|
],
|
|
)
|
|
def test_same_day_window(self, moment: time, *, expected: bool) -> None:
|
|
"""A non-wrapping window is a plain half-open interval."""
|
|
window = CurfewWindow(start=time(9, 0), end=time(17, 0))
|
|
assert window.contains(moment) is expected
|
|
|
|
|
|
class TestFocusPolicy:
|
|
"""Allow/deny decisions and the invariants that keep the phone usable."""
|
|
|
|
def test_rejects_launcher_outside_allowlist(self) -> None:
|
|
"""A policy that blocks its own launcher would leave no home screen."""
|
|
with pytest.raises(PolicyError, match="no home screen"):
|
|
_policy(launcher_package="com.missing")
|
|
|
|
def test_accepts_launcher_inside_allowlist(self) -> None:
|
|
"""The guard permits a launcher that is actually allowed."""
|
|
assert _policy(launcher_package="com.launcher").launcher_package
|
|
|
|
def test_rejects_night_packages_absent_from_day_list(self) -> None:
|
|
"""A night allowance for a day-blocked package is contradictory."""
|
|
with pytest.raises(PolicyError, match="subset"):
|
|
_policy(night_allowed_packages=frozenset({"com.unknown"}))
|
|
|
|
@pytest.mark.parametrize(
|
|
("package", "expected"),
|
|
[
|
|
("com.android.settings", True),
|
|
("com.android.providers", True),
|
|
("com.android.providers.telephony", True),
|
|
("com.android.providersomething", False),
|
|
("com.good", False),
|
|
],
|
|
)
|
|
def test_protection_matches_whole_labels_only(
|
|
self,
|
|
package: str,
|
|
*,
|
|
expected: bool,
|
|
) -> None:
|
|
"""Prefix protection covers subpackages but not lookalike names.
|
|
|
|
``com.android.providers.telephony`` must be protected; the unrelated
|
|
``com.android.providersomething`` must not be swept in by a bare
|
|
string prefix test.
|
|
"""
|
|
assert _policy().is_protected(package) is expected
|
|
|
|
def test_protected_packages_are_allowed_even_during_curfew(self) -> None:
|
|
"""Core system packages survive the strictest state."""
|
|
policy = _policy()
|
|
assert policy.is_allowed("com.android.settings", during_curfew=True)
|
|
|
|
def test_curfew_narrows_the_allowlist(self) -> None:
|
|
"""Day-allowed apps can still be blocked at night."""
|
|
policy = _policy()
|
|
assert policy.is_allowed("com.good")
|
|
assert not policy.is_allowed("com.good", during_curfew=True)
|
|
assert policy.is_allowed("pl.mbank", during_curfew=True)
|
|
|
|
def test_unknown_package_is_blocked(self) -> None:
|
|
"""Anything not explicitly allowed is denied."""
|
|
assert not _policy().is_allowed("com.evil")
|
|
|
|
def test_is_curfew_active_without_window_is_false(self) -> None:
|
|
"""A policy with no curfew is never in curfew."""
|
|
assert not _policy().is_curfew_active(time(23, 30))
|
|
|
|
def test_is_curfew_active_with_window(self) -> None:
|
|
"""A configured window drives the curfew state."""
|
|
policy = _policy(curfew=CurfewWindow(start=time(23, 0), end=time(5, 0)))
|
|
assert policy.is_curfew_active(time(23, 30))
|
|
assert not policy.is_curfew_active(time(12, 0))
|
|
|
|
def test_packages_to_block_ignores_uninstalled(self) -> None:
|
|
"""Only packages present on the device can be blocked."""
|
|
policy = _policy()
|
|
installed = frozenset({"com.good", "com.evil", "com.android.settings"})
|
|
assert policy.packages_to_block(installed) == frozenset({"com.evil"})
|
|
|
|
def test_packages_to_block_during_curfew(self) -> None:
|
|
"""The curfew list is applied when the curfew flag is set."""
|
|
policy = _policy()
|
|
installed = frozenset({"com.good", "pl.mbank"})
|
|
assert policy.packages_to_block(installed, during_curfew=True) == frozenset(
|
|
{"com.good"},
|
|
)
|