diff --git a/.gitignore b/.gitignore index 7c271f773..05e37ff43 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,6 @@ AndroidRunner/Plugins/monsoon/script/monsoon_config.csv # ignore any ouputs of experiments output/ + +# Per-machine device-state capability cache (E0.T8) — keyed by adb serial +examples/batterymanager/Scripts/.device_state_capabilities/ diff --git a/AndroidRunner/NativeExperiment.py b/AndroidRunner/NativeExperiment.py index 0a770112d..c1bb5f628 100644 --- a/AndroidRunner/NativeExperiment.py +++ b/AndroidRunner/NativeExperiment.py @@ -13,7 +13,15 @@ def __init__(self, config, progress, restart): self.autostart_subject = config.get('autostart_subject', True) self.experiment_args = config.get('experiment_args', [0]) # Just a single argument, if none are specified super(NativeExperiment, self).__init__(config, progress, restart) + # If True, the interaction script blocks for the full ``duration`` window itself (e.g. sysfs loop, + # or Appium subprocess hold — see interaction_appium_metronome.py). Skip the extra sleep below so + # the profiled window is not doubled. + self.interaction_covers_duration = bool(config.get('interaction_covers_duration', False)) self.pre_installed_apps = config.get('apps', []) + # When installing from ``paths``, the runner otherwise derives the package name from the APK + # filename (splitext basename). Obfuscated or packed builds often use unrelated filenames; + # set ``application_id`` to the manifest packageName (e.g. com.bobek.metronome). + self.application_id = config.get('application_id') for apk in config.get('paths', []): if not op.isfile(apk): raise ConfigError('File %s not found' % apk) @@ -33,9 +41,10 @@ def before_run_subject(self, device, path, *args, **kwargs): else: filename = op.basename(path) self.logger.info('APK: %s' % filename) - if filename not in device.get_app_list(): + pkg = self.application_id or op.splitext(filename)[0] + if pkg not in device.get_app_list(): device.install(path) - self.package = op.splitext(op.basename(path))[0] + self.package = pkg def get_run_count(self): return self.repetitions * len(self.experiment_args) @@ -53,7 +62,8 @@ def start_profiling(self, device, path, run, *args, **kwargs): def interaction(self, device, path, run, *args, **kwargs): super(NativeExperiment, self).interaction(device, path, run, *args, **kwargs) - time.sleep(self.duration) + if not self.interaction_covers_duration: + time.sleep(self.duration) def after_run(self, device, path, run, *args, **kwargs): self.before_close(device, path, run) diff --git a/AndroidRunner/Plugins/android/Android.py b/AndroidRunner/Plugins/android/Android.py index bcb070162..a72cc5551 100644 --- a/AndroidRunner/Plugins/android/Android.py +++ b/AndroidRunner/Plugins/android/Android.py @@ -70,22 +70,30 @@ def start_profiling(self, device, **kwargs): def get_data(self, device, app): """Runs the profiling methods every self.interval seconds in a separate thread""" self.lock.acquire() - if not self.profile: + try: + if not self.profile: + return + start = timeit.default_timer() + try: + device_time = device.shell('date -u') + row = [device_time] + if 'cpu' in self.data_points: + row.append(self.get_cpu_usage(device)) + if 'mem' in self.data_points: + row.append(self.get_mem_usage(device, app)) + self.data.append(row) + except Exception: + # Transient errors (e.g. "No process found for " during a brief + # terminate_app+activate_app recovery in the interaction script) must + # NOT leak the lock — otherwise stop_profiling() will deadlock and + # teardown hangs forever. Swallow and let the next Timer retry. + pass + end = timeit.default_timer() + interval = max(float(0), self.interval - max(0, int(end - start))) + finally: self.lock.release() - return - start = timeit.default_timer() - device_time = device.shell('date -u') - row = [device_time] - if 'cpu' in self.data_points: - row.append(self.get_cpu_usage(device)) - if 'mem' in self.data_points: - row.append(self.get_mem_usage(device, app)) - self.data.append(row) - end = timeit.default_timer() - # timer results could be negative - interval = max(float(0), self.interval - max(0, int(end - start))) - self.lock.release() - threading.Timer(interval, self.get_data, args=(device, app)).start() + if self.profile: + threading.Timer(interval, self.get_data, args=(device, app)).start() def stop_profiling(self, device, **kwargs): self.lock.acquire() diff --git a/AndroidRunner/Plugins/batterymanager/Batterymanager.py b/AndroidRunner/Plugins/batterymanager/Batterymanager.py index 786ac630b..949a2cd7d 100644 --- a/AndroidRunner/Plugins/batterymanager/Batterymanager.py +++ b/AndroidRunner/Plugins/batterymanager/Batterymanager.py @@ -185,7 +185,12 @@ def calculate_power(df): @staticmethod def trapezoid_method(df): - return np.trapz(df['power'].values, df['Timestamp'].values) + # NumPy 2.0+ renamed trapz to trapezoid; keep both for venvs on 1.x and 2.x + y, x = df['power'].values, df['Timestamp'].values + trapezoid = getattr(np, "trapezoid", None) or getattr(np, "trapz", None) + if trapezoid is None: + raise RuntimeError("NumPy trapezoidal integration API not found") + return trapezoid(y, x) @staticmethod def aggregate_batterymanager_runs(logs_dir): @@ -211,7 +216,7 @@ def aggregate_batterymanager_runs(logs_dir): runs = pd.concat([runs, pd.DataFrame(stats, index=[0])], ignore_index=True) - runs = runs.drop(columns=['Timestamp', 'power'], axis=1) + runs = runs.drop(columns=['Timestamp', 'power'], errors='ignore') return runs @staticmethod diff --git a/devices.json b/devices.json index ec87314ce..cb70c5e15 100644 --- a/devices.json +++ b/devices.json @@ -14,5 +14,8 @@ "GalaxyJ7-W": "192.168.0.106:5555", "Nexus 4": "emulator-5554", - "Pixel 6": "emulator-5554" + "Pixel 3": "89WX0HWVF", + "Pixel 6": "18131FDF6002S9", + "Pixel 9": "56040DLAQ0027U", + "Pixel 9-W": "10.15.10.93:5555" } diff --git a/examples/batterymanager/ESPRESSO_MIRROR_VALIDATION.md b/examples/batterymanager/ESPRESSO_MIRROR_VALIDATION.md new file mode 100644 index 000000000..82266024e --- /dev/null +++ b/examples/batterymanager/ESPRESSO_MIRROR_VALIDATION.md @@ -0,0 +1,100 @@ +# Espresso vs Appium “espresso_mirror” workload — validation matrix + +This document supports **thesis / committee validation**: it states how closely the Android Runner +Appium workload (`APPIUM_WORKLOAD=espresso_mirror`, hook +`Scripts/interaction_appium_metronome_espresso_mirror.py`) relates to the **upstream** Metronome +**Espresso** suite. + +**Source of truth (Espresso)** +- `app/src/androidTest/java/com/bobek/metronome/InstrumentedTest.kt` +- `app/src/androidTest/java/com/bobek/metronome/AbstractAndroidTest.kt` (shared `R.id` helpers, `applyTempo`, `verifyTempoMarking`) + +**Appium implementation** +- `Scripts/interaction_appium_metronome.py` — function `run_espresso_mirror_workload` (and helpers such as `_type_numeric_edit_by_id`, `_ESPRESSO_TEMPO_MARKING_WALK`, UI pulse fillers) + +--- + +## 1. Methodology difference (all scenarios) + +| Aspect | Espresso (`androidTest`) | Appium (black-box) | +|--------|-------------------------|-------------------| +| **APK** | Typically debug + **test runner** on device/emulator | Same **installable** app id (`com.bobek.metronome`) as user experiments (debug / R8 / …) | +| **Selectors** | `withId(R.id.*)`, `SliderUtils.setValue`, Hamcrest | `find_element(ID, "package:id/…")`, `UiAutomator` text/description for pulse | +| **Tempo / beats / subdivisions** | Often **slider** `setValue` then **assert** edit + slider | We **type into `*_edit` fields** (equivalent end-state if UI sync matches Espresso’s coupling tests; **not** identical gestures for energy) | +| **Assertions** | Strict (`matches(withText(…))`, `displaysError()`, slider `withValue`) | **Soft / logging**: booleans in `appium_workload_coverage.jsonl`, optional substring checks on `tempo_marking_text` | +| **Error tests** | Assert `TextInputLayout` shows error | **“Touch only”**: enter invalid text then restore — **does not** assert error drawable/state | + +**Validation claim:** The Appium suite is **scenario-aligned** (same screens and IDs, same numeric journeys where possible), **not** a byte-for-byte replay of Espresso gestures or assertions. + +--- + +## 2. Scenario-by-scenario comparison + +### Legend + +- **Close** — Same logical steps and views; gesture path may differ (edit vs slider). +- **Partial** — Subset of Espresso steps, or substring check instead of exact `withText(R.string.…)`. +- **Touch-only** — Same inputs as Espresso’s “bad value” phase; **no** Espresso-style error UI assertion. +- **Not implemented** — No dedicated Appium scenario (may overlap indirectly). + +| Espresso `@Test` (InstrumentedTest) | Appium `scenario_name` | Relationship | Notes | +|------------------------------------|-------------------------|--------------|--------| +| `contentVisible` | *(none)* | **Not implemented** | Espresso checks `loading_indicator` gone + `content` visible. Appium uses `await_app_ready` (FAB / strings), not the same view IDs. | +| `initialState` | `initialState` | **Partial / Close** | Espresso: sliders → 4, 1, `applyTempo(80)` + **many** `check()` on sliders/edits/markings. Appium: types `4`, `1`, `80` into edits + checks **Andante** substring on `tempo_marking_text`. Does **not** assert slider `withValue`. | +| `beatsSliderAndEditReflectEachOther` | `beatsReflect` | **Close** | Espresso: slider 1 → edit shows `1` → `replaceText("2")` → slider `withValue(2)`. Appium: `beats_edit` `1` → `2` only (skips explicit slider drag). | +| `subdivisionsSliderAndEditReflectEachOther` | `subdivisionsReflect` | **Close** | Same pattern as beats; edit-only path. | +| `tempoSliderAndEditReflectEachOther` | `tempoReflect` | **Close** | Espresso: slider 30 → edit `30` → `replaceText("40")`. Appium: `tempo_edit` `30` → `40`. | +| `tempoMarkings` | `tempoMarkingsWalk` | **Partial** | Espresso: **18** `applyTempo` / `verifyTempoMarking` pairs (exact string resource match). Appium: **8** tempo checkpoints with **English** substring defaults (`_ESPRESSO_TEMPO_MARKING_WALK`). Missing intermediate tempos (e.g. 59, 65, 75, 107, 119, 167, 169, 252). Env vars `METRONOME_TEMPO_MARKING_*_SUBSTR` for locale. | +| `beatsErrorWhenValueTooBig` | `invalidBeatsReset` | **Touch-only** | Espresso: slider 1, type `9`, **assert** `beats_edit_layout` error + slider unchanged. Appium: `1` → `9` → restore `4`; **no** layout error assertion. | +| `beatsErrorWhenValueNotANumber` | `beatsErrorNonNumericTouch` | **Touch-only** | Espresso: type `.`, assert error. Appium: `1` → `.` → restore `4`. | +| `subdivisionsErrorWhenValueTooBig` | `subdivisionsErrorTooBigTouch` | **Touch-only** | Espresso: subdivisions `5` invalid from base 1. Appium: `1` → `5` → restore `2` (same restore shape as our suite, not necessarily Espresso’s implied “2”). | +| `subdivisionsErrorWhenValueNotANumber` | *(none)* | **Not implemented** | Could add mirror of `.` + restore if needed. | +| `tempoErrorWhenValueTooBig` | `tempoErrorTooBigTouch` | **Touch-only** | Espresso: tempo 30, `253`, assert error on layout. Appium: `30` → `253` → restore `80`. | +| `tempoErrorWhenValueNotANumber` | *(none)* | **Not implemented** | Same gap as subdivisions non-number for tempo field. | + +--- + +## 3. What Appium adds that Espresso does not define as `@Test` + +| Mechanism | Purpose | +|-----------|---------| +| **`_espresso_mirror_ui_pulse_until_near_deadline`** | Keeps **continuous UI** (FAB, tempo ±, tick viz, tap tempo, swipes) between suite rounds until ~8s before workload deadline — fills JSON `duration` with interaction. | +| **`_espresso_mirror_ui_pulse_final_gap`** | Uses last ~second(s) before deadline. | +| **Baseline workload** (`APPIUM_WORKLOAD=metronome`) | Separate longer structured tap loop — **not** Espresso-mapped; documented elsewhere. | + +These are **energy / workload saturations**, not claims of Espresso parity. + +--- + +## 4. Summary counts (for validation slides) + +| Category | Count | +|----------|-------| +| Espresso `@Test` methods in `InstrumentedTest` | **13** | +| Named Appium espresso_mirror scenarios per suite round | **9** | +| Espresso tests with a **dedicated** Appium analogue (full / partial / touch-only) | **9** of **13** (`contentVisible`, `subdivisionsErrorWhenValueNotANumber`, `tempoErrorWhenValueNotANumber` have **no** dedicated scenario) | +| `tempoMarkings` | Espresso **18** checkpoints → Appium **8** tempo stops in `_ESPRESSO_TEMPO_MARKING_WALK` (subset) | + +--- + +## 5. Suggested wording for a thesis / defence + +> We automated the **same Metronome UI surfaces** addressed in `InstrumentedTest`, using **resource IDs** +> and **edit fields** on the **same APK** used in energy experiments. Where Espresso asserts internal +> slider positions and `TextInputLayout` errors, our black-box driver records **success booleans** and +> **optional** marking substrings. **Gesture paths** differ (typing edits vs `SliderUtils.setValue`) +> but align with the app’s **slider–edit coupling** tests as behavioural intent. **Continuous tap/swipe** +> fillers occupy remaining experiment time without claiming equivalence to a specific Espresso `@Test`. + +--- + +## 6. References (paths in this repo workspace) + +- Espresso: + `app_repositories_newest/app_repositories/Kr0oked_Metronome/app/src/androidTest/java/com/bobek/metronome/InstrumentedTest.kt` +- Appium mirror: + `android-runner/examples/batterymanager/Scripts/interaction_appium_metronome.py` (`run_espresso_mirror_workload`) +- Thin hook: + `android-runner/examples/batterymanager/Scripts/interaction_appium_metronome_espresso_mirror.py` + +*Generated for validation; update this file if scenarios or `_ESPRESSO_TEMPO_MARKING_WALK` change.* diff --git a/examples/batterymanager/README-experiments.md b/examples/batterymanager/README-experiments.md new file mode 100644 index 000000000..58f54626f --- /dev/null +++ b/examples/batterymanager/README-experiments.md @@ -0,0 +1,288 @@ +# Monkey + sysfs power measurement (black-box) + +This folder contains a **black-box experiment pipeline** that: + +- installs/launches a target Android app (by package name) +- generates interaction/workload using **Android Monkey** +- **samples battery current + voltage via sysfs** during the workload +- computes **power (W)** and **energy (J)** from the samples + +It was built to work even when developer tests (Espresso) are unreliable and when the Android Runner BatteryManager +companion app cannot run on newer Android versions due to privileged-permission restrictions. + +--- + +## Why sysfs (and not the BatteryManager plugin)? + +Android Runner’s BatteryManager profiler requires a companion app (`com.example.batterymanager_utility`). +On Android 15 (Pixel 9 in this setup), the companion app may crash with: + +- `java.lang.SecurityException: ... does not have android.permission.BATTERY_STATS` + +`BATTERY_STATS` is a privileged/signature permission on many builds, so a normal user-installed app cannot obtain it. + +To keep the pipeline **unprivileged** and **black-box**, we read current/voltage directly from: + +- `/sys/class/power_supply/battery/current_now` +- `/sys/class/power_supply/battery/voltage_now` + +These are kernel-exposed power-supply readings (sysfs). + +--- + +## What you run + +### 1) Install the app under test + +Example for Metronome: + +```bash +adb install -r "/home/irena/Documents/Master Experiment/app_repositories_newest/app_repositories/Kr0oked_Metronome/app/build/outputs/apk/debug/app-debug.apk" +``` + +**Bangcle-protected, manually signed Metronome (same package id):** install on **Pixel 3** (serial `89WX0HWVF` in `devices.json` as `"Pixel 3"`). The signed APK used here: + +`/home/irena/Documents/Master Thesis/APKs/app-protected-signed.apk` + +```bash +adb -s 89WX0HWVF install -r "/home/irena/Documents/Master Thesis/APKs/app-protected-signed.apk" +``` + +Then run `examples/batterymanager/monkey_experiment_pixel3.json` (still `com.bobek.metronome` in `"apps"`). + +### 2) Run the experiment + +```bash +cd "/home/irena/Documents/Master Experiment/android-runner" +source .venv/bin/activate +python3 __main__.py examples/batterymanager/monkey_experiment.json +``` + +For **Pixel 3** (e.g. Bangcle build above): `python3 __main__.py examples/batterymanager/monkey_experiment_pixel3.json` + +**Pixel 3 + Monkey + BatteryManager plugin only (no sysfs):** use `monkey_experiment_pixel3_batterymanager.json`, which sets `profilers.batterymanager` and `interaction` to `Scripts/interaction_monkey_only.py` (Monkey in the background; power comes from the plugin, not from `sysfs_power_*.csv`). + +```bash +python3 __main__.py examples/batterymanager/monkey_experiment_pixel3_batterymanager.json +``` + +Install the [BatteryManager companion](https://github.com/S2-group/batterymanager-companion/releases) on the device (`com.example.batterymanager_utility`) and grant its permissions. On some OS levels (e.g. Android 15) the plugin may still fail; **Android 12 on Pixel 3** is a reasonable place to try `adb_log` mode. + +| Config | Device | Energy data | +|--------|--------|-------------| +| `monkey_experiment.json` | Pixel 9 (in file) | sysfs + `compute_energy_from_sysfs.py` | +| `monkey_experiment_pixel3.json` | Pixel 3 | sysfs + `compute_energy_from_sysfs.py` | +| `monkey_experiment_pixel3_batterymanager.json` | Pixel 3 | BatteryManager plugin (logcat → per-run CSV; `Aggregated_Results_Batterymanager.csv` at end of run) | +| `monkey_experiment_pixel3_appium_metronome.json` | Pixel 3 | **Appium** workload (`Scripts/interaction_appium_metronome.py`) instead of Monkey; install `requirements-appium.txt`, run `appium` on the host | +| `monkey_experiment_pixel3_appium_metronome_batterymanager.json` | Pixel 3 | Same Appium workload + **BatteryManager** profiler (no sysfs CSV) | + +**BatteryManager: what we observed in this project** + +- On **Pixel 9 / Android 15**, the companion app can fail (e.g. `BATTERY_STATS` / privileged permissions), so the **sysfs** path is the reliable default there. +- On **Pixel 3 / Android 12 (API 31)**, a full run with `monkey_experiment_pixel3_batterymanager.json` **succeeded**: the companion started the data-collection service, the plugin produced per-run `logcat__*.csv` under `data/.../batterymanager/`, and after aggregation the run folder contained `Aggregated_Results_Batterymanager.csv` (example output folder: `output/2026.04.24_131804/`). +- The stock Android Runner plugin needed small **compatibility fixes** in this environment (NumPy 2.x: `trapz` → `trapezoid`; pandas: `drop` without `axis` together with `columns`). Those changes live in `AndroidRunner/Plugins/batterymanager/Batterymanager.py` in this tree. + +Notes: +- `examples/batterymanager/monkey_experiment.json` pins `adb_path` so the run does not depend on your shell `PATH`. +- The device name in the config must exist in `android-runner/devices.json`. + +--- + +## What the experiment does + +The experiment is configured as a **native** Android Runner experiment (`type: "native"`): + +- **Subject**: the app package name in `"apps"` (e.g. `com.bobek.metronome`) +- **Repetitions**: `"repetitions": 2` → two independent runs +- **Duration**: `"duration": 60000` ms → ~60 seconds per run + +Workload is usually **Monkey**, unless you point `"interaction"` at another script; where energy numbers come from depends on the `interaction` script and `profilers`: + +- **Default** (`Scripts/interaction.py`): same Monkey command, then **sysfs** sampling to `sysfs_power_*.csv` (see [Output layout](#output-layout)). Use when you want the host-side script `compute_energy_from_sysfs.py` and no dependency on the companion app. +- **Monkey only** (`Scripts/interaction_monkey_only.py`): starts Monkey only. Use with `"profilers": { "batterymanager": ... }` so the Android Runner **BatteryManager** plugin collects `BATTERY_PROPERTY_CURRENT_NOW` / `EXTRA_VOLTAGE` (via the companion and `adb_log` persistency by default in that config). No `sysfs_power_*.csv` is written. +- **Appium (Metronome)** (`Scripts/interaction_appium_metronome.py`): **UiAutomator2** workload on the device via an Appium server on the host (`APPIUM_SERVER_URL`, default `http://127.0.0.1:4723`). Uses `device.id` as `udid`. Runs in a **background thread** so the native experiment’s duration sleep still defines the measured window (same pattern as background Monkey). Install deps: `pip install -r requirements-appium.txt`. Details and step-by-step: `appium_android_tests/README.md`. + +--- + +## Methodology note: foreground app (documented; script update pending) + +**Issue.** If `before_run.py` starts the BatteryManager **companion** (`com.example.batterymanager_utility`), the **screen can stay on that utility** for much of the run while Monkey still targets the **subject** package (e.g. `com.bobek.metronome`). Whole-device sysfs energy then mixes the wrong foreground UI with the workload you intend to compare. That **pollutes** comparisons such as **debug vs R8 vs Allatori**—the confound is not the obfuscator, it is “utility on screen vs app on screen.” + +**Intended pipeline** (to apply in the Android Runner scripts when you have time; companion code can stay in the tree for a future BatteryManager-profiler attempt): + +1. **Before run:** `am force-stop` the **target** package, then **launch** the target app (e.g. `adb shell monkey -p -c android.intent.category.LAUNCHER 1`, or resolve the launcher activity with `cmd package resolve-activity`). +2. **Interaction:** unchanged—start Monkey for the target package and sample sysfs (see `Scripts/interaction.py`). +3. **After run:** `am force-stop` the **target** package only. Do not start or force-stop the BatteryManager utility as part of the default sysfs path. + +**Logging** (when implemented): log the package launched, when Monkey starts, and the path of the sysfs CSV so experiment logs are easy to audit. + +**BatteryManager profiler:** sysfs measurement does **not** require the companion. Keep any BatteryManager-related setup in docs or optional configs for when you try the **plugin** again on a build where the companion is usable. + +--- + +## Output layout + +Android Runner writes outputs under: + +`examples/batterymanager/output//` + +Inside the run folder, the sysfs sample CSVs are under: + +`data///sysfs_power__.csv` + +Example: + +`output/2026.04.24_111912/data/Pixel 9/com-bobek-metronome/sysfs_power_56040DLAQ0027U_2026.04.24_112134.csv` + +Each file corresponds to **one repetition**. + +If you use the **BatteryManager** profiler, Android Runner also writes plugin outputs under the same run (e.g. logcat-derived CSV and plugin aggregation) under paths like `data///batterymanager/` (see the plugin’s behaviour in `AndroidRunner/Plugins/batterymanager/`). + +--- + +## What is measured (units) + +Each `sysfs_power_*.csv` contains: + +- `epoch_ms`: wall-clock timestamp (milliseconds since epoch) +- `current_now_ua`: battery current (microamps, µA) + - can be **negative** while discharging (device-dependent convention) +- `voltage_now_uv`: battery voltage (microvolts, µV) + +--- + +## How power and energy are calculated + +We treat each sample as an approximate instantaneous battery-side measurement: + +### Convert units + +- \(I\,[A] = \text{current\_now\_ua} \times 10^{-6}\) +- \(V\,[V] = \text{voltage\_now\_uv} \times 10^{-6}\) +- \(t\,[s] = \text{epoch\_ms} / 1000\) + +### Instantaneous power (Watts) + +\[ +P(t)\,[W] \approx |I(t)| \cdot V(t) +\] + +We use \(|I|\) so discharging current yields positive power draw. + +### Energy over the run (Joules) + +Energy is the integral of power over time: + +\[ +E\,[J] = \int P(t)\,dt +\] + +With discrete samples, we compute \(E\) using the **trapezoidal rule**: + +\[ +E \approx \sum_i \frac{P_i + P_{i+1}}{2} \cdot (t_{i+1} - t_i) +\] + +### Average power (Watts) + +\[ +\bar{P} = \frac{E}{T} +\] + +where \(T\) is the run duration in seconds. + +### BatteryManager plugin path (Android Runner aggregation) + +This applies when you use `profilers.batterymanager` and the same data points as in `monkey_experiment_pixel3_batterymanager.json` (`BATTERY_PROPERTY_CURRENT_NOW`, `EXTRA_VOLTAGE`, persistency `adb_log`). The companion streams samples; the plugin parses logcat into a CSV, then **aggregates** in `Batterymanager.aggregate_batterymanager_runs` (see `AndroidRunner/Plugins/batterymanager/Batterymanager.py`). + +**1) Time axis** + +- The `Timestamp` column is shifted so the first sample is at **0**; the plugin then divides by **1000** (Android Runner’s convention there is “raw → axis used for integration”; treat the result as the time coordinate in seconds for integration together with the power series). + +**2) Per-sample power (W)** + +The plugin uses the absolute value of instantaneous current and voltage from the BatteryManager API, converted to **amperes** and **volts**: + +- `BATTERY_PROPERTY_CURRENT_NOW` is in **microamperes** (µA) in the API; the code divides by \(10^6\) to get **A**. +- `EXTRA_VOLTAGE` is taken as **millivolts** (mV) relative to the formula in code; the code divides by **1000** to get **V** (check your Android version and companion logs if you need exact SI traceability in the write-up). + +\[ +P(t)\,[W] = \lvert I(t) \rvert\,[A] \cdot V(t)\,[V] +\] + +Implemented as: + +\[ +P = \frac{\lvert \text{BATTERY\_PROPERTY\_CURRENT\_NOW} \rvert}{10^6} \cdot \frac{\text{EXTRA\_VOLTAGE}}{10^3} +\] + +(Exact division layout matches the Java/Android Runner `calculate_power` in `Batterymanager.py`.) + +**3) `Avg power (W)`** + +- Arithmetic mean of the per-sample `power` column for that run: \(\bar{P} = \text{mean}(P_i)\). + +**4) `Energy simple (J)`** + +- **Not** the trapezoidal integral. It is the **rectangle rule** using the **average** power and the **last** (relative) timestamp as run length: + +\[ +E_{\text{simple}} = \bar{P} \cdot t_{\text{end}} +\] + +where \(t_{\text{end}} = \max_i(\text{Timestamp}_i)\) after preprocessing. + +This equals \(\int P\,dt\) only if power were constant; otherwise it can differ from `Energy trapz (J)`. + +**5) `Energy trapz (J)`** + +- Trapezoidal integration of **power vs time** for that run (NumPy `trapezoid` with fallback to `trapz` in our patched code): + +\[ +E_{\text{trapz}} \approx \int P(t)\,dt \quad \text{on the discrete } (t_i, P_i) \text{ series.} +\] + +This is the same **idea** as the sysfs `compute_energy_from_sysfs.py` trapezoidal energy, but using the companion’s sampling and column layout instead of `sysfs_power_*.csv`. + +**6) `Aggregated_Results_Batterymanager.csv`** + +- One row per run (per device/app/repetition), with columns such as `Avg power (W)`, `Energy simple (J)`, `Energy trapz (J)`, and meaned raw fields, written when the experiment finishes and **final aggregation** runs. If that step errors, re-run with `--progress` pointing at the run’s `progress.xml` after fixing the code (as done for the Pixel 3 Bangcle run). + +--- + +## Computing the summary CSV + +Script: + +- `compute_energy_from_sysfs.py` + +Run it on a specific Android Runner output folder: + +```bash +python3 examples/batterymanager/compute_energy_from_sysfs.py \ + "examples/batterymanager/output/2026.04.24_111912" +``` + +It writes: + +- `sysfs_energy_summary.csv` (one row per `sysfs_power_*.csv`) + +Optional: + +```bash +python3 examples/batterymanager/compute_energy_from_sysfs.py \ + "examples/batterymanager/output/2026.04.24_111912" \ + --write-power-csv +``` + +This also writes per-sample CSVs containing `power_w`. + +--- + +## Limitations / caveats to mention in a write-up + +- **Battery-side approximation**: current/voltage are battery-level readings; they approximate device power draw, but are not as accurate as external hardware (Monsoon). +- **Sampling granularity**: sysfs sampling at 100 ms is “best effort”; actual timing can drift with device load. +- **Kernel/vendor differences**: some devices expose different units or paths; Pixel 9 works with the paths above. +- **Monkey reproducibility**: fixed seed (`-s 1234`) improves reproducibility, but UI timing/network conditions still introduce variance. + diff --git a/examples/batterymanager/README-templates.md b/examples/batterymanager/README-templates.md new file mode 100644 index 000000000..90059e621 --- /dev/null +++ b/examples/batterymanager/README-templates.md @@ -0,0 +1,205 @@ +# Per-device + per-(app, variant) experiment templates + +This folder (`android-runner/examples/batterymanager/_templates/`) holds the +**parametric** Android Runner JSON templates used to generate every +`(app, variant, device)` experiment in the Master's-thesis matrix without +hand-copying a 1 KB JSON ~200+ times. + +The convention is intentionally boring: + +- **Device profiles** (`device_pixel3.json`, `device_pixel6.json`, + `device_pixel9.json`) describe one physical device and capture all the + per-device gotchas the agent has hit (ABI compatibility, BatteryManager + status, required adb permission grants, sysfs fallback rule). +- **App-variant template** (`app_variant_2min.json`) is one canonical Android + Runner experiment skeleton with **5 string placeholders** that get filled in + per `(app, variant, device)`. The structural reference is + [`monkey_espresso_mirror_2min_baseline.json`](../monkey_espresso_mirror_2min_baseline.json) + — identical `profilers.batterymanager.*` block. + +Locked experiment invariants in the template (do not vary across the matrix): + +| Field | Value | Meaning | +| ------------------------------ | --------- | ------------------------------------------------------------------------ | +| `duration` | `120000` | 2-minute fixed scenario window per run (cross-variant comparability). | +| `repetitions` | `3` | 3 measurement repetitions per `(app, variant, device)`. | +| `interaction_covers_duration` | `true` | The interaction script keeps the workload running for the full duration. | +| `time_between_run` | `5000` | 5 s settle between repetitions. | +| `profilers.batterymanager.*` | as-is | Same shape as the canonical 2-min baseline config. | +| `scripts.*` | full set | All lifecycle hooks present (uninstall / before / after / interaction). | + +## Placeholders + +The template uses **double-curly-brace** placeholders so they're trivially +matched by `sed`/`grep`/`str.replace`: + +| Placeholder | Meaning | Example | +| --------------------- | ---------------------------------------------------------- | -------------------------------------------------------- | +| `{{APP_ID}}` | Short app key used in filenames and the per-app uninstall hook script name. | `metronome` | +| `{{APK_PATH}}` | Absolute path to the variant APK to install. | `/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/Kr0oked_Metronome/baseline/app-debug.apk` | +| `{{APPLICATION_ID}}` | Manifest package name (used by Android Runner as `application_id`). For packed APKs this is the **manifest** package, not the filename. | `com.bobek.metronome` | +| `{{DEVICE_NAME}}` | Android Runner `devices` block key — must match the device profile JSON. | `Pixel 3` / `Pixel 6` / `Pixel 9` | +| `{{INTERACTION_HOOK}}`| Relative path under `Scripts/` for the per-app Appium interaction script (Espresso-mirrored, black-box). | `Scripts/interaction_appium_metronome_espresso_mirror.py` | + +The serial placeholders inside the device profiles +(`{{PIXEL3_SERIAL}}`, `{{PIXEL6_SERIAL}}`, `{{PIXEL9_SERIAL}}`) are **not** +substituted into the experiment JSON — Android Runner picks the connected +device via `adb` using the `devices` block key (e.g. `"Pixel 3": {}`). The +serial placeholders exist so adb commands documented in the device profile +(`adb -s shell pm grant ...`) can be replayed by the matrix runner +without ambiguity when more than one device is connected. + +## How to materialize one concrete config + +For a single `(app, variant, device)` cell of the matrix you have two equally +valid options. + +### Option A: tiny Python helper (recommended for the E5 matrix sweep) + +```python +import json +from pathlib import Path + +TEMPLATE_DIR = Path("android-runner/examples/batterymanager/_templates") +OUT_DIR = Path("android-runner/examples/batterymanager/generated") + +def materialize(app_id: str, + apk_path: str, + application_id: str, + device_name: str, + interaction_hook: str, + variant: str) -> Path: + raw = (TEMPLATE_DIR / "app_variant_2min.json").read_text() + filled = (raw + .replace("{{APP_ID}}", app_id) + .replace("{{APK_PATH}}", apk_path) + .replace("{{APPLICATION_ID}}", application_id) + .replace("{{DEVICE_NAME}}", device_name) + .replace("{{INTERACTION_HOOK}}", interaction_hook)) + config = json.loads(filled) + OUT_DIR.mkdir(parents=True, exist_ok=True) + out_name = f"{app_id}_{variant}_{device_name.lower().replace(' ', '')}.json" + out_path = OUT_DIR / out_name + out_path.write_text(json.dumps(config, indent=2)) + return out_path +``` + +Example call for `(metronome, baseline, Pixel 3)`: + +```python +materialize( + app_id = "metronome", + apk_path = "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/Kr0oked_Metronome/baseline/app-debug.apk", + application_id = "com.bobek.metronome", + device_name = "Pixel 3", + interaction_hook = "Scripts/interaction_appium_metronome_espresso_mirror.py", + variant = "baseline", +) +``` + +That single call produces a fully valid Android Runner JSON identical in +structure to `monkey_espresso_mirror_2min_baseline.json`, ready to feed to +`python android_runner.py `. + +### Option B: one-liner `sed` for ad-hoc materialization + +```bash +sed \ + -e 's|{{APP_ID}}|metronome|g' \ + -e 's|{{APK_PATH}}|/abs/path/to/app-debug.apk|g' \ + -e 's|{{APPLICATION_ID}}|com.bobek.metronome|g' \ + -e 's|{{DEVICE_NAME}}|Pixel 3|g' \ + -e 's|{{INTERACTION_HOOK}}|Scripts/interaction_appium_metronome_espresso_mirror.py|g' \ + android-runner/examples/batterymanager/_templates/app_variant_2min.json \ + > android-runner/examples/batterymanager/generated/metronome_baseline_pixel3.json +``` + +Then `python -m json.tool generated/metronome_baseline_pixel3.json` should +exit clean — that's the cheapest validation that all placeholders were +substituted. + +## E5 (matrix sweep) convention + +Epic E5 will programmatically generate **~200+ concrete experiment configs** +from this single template by sweeping the cross-product: + +``` +final_dataset apps × {baseline, obfuscated, packed} × {Pixel 3, Pixel 6, Pixel 9} +``` + +Concretely E5 will: + +1. Walk `app_repositories_newest/final_dataset/` to enumerate apps. +2. For each app, look up its variant APK paths and its + `interaction_appium__espresso_mirror.py` hook. +3. For each `(app, variant, device)` triple, call the `materialize()` helper + above (or equivalent), writing the output to a `generated/` subfolder. +4. Per-device gotchas (BATTERY_STATS grant, sysfs fallback, ABI mismatch) are + pulled from the corresponding `device_pixelN.json` profile and recorded + in the run log — they do **not** mutate the experiment JSON itself. The + experiment JSON stays a thin parametric shell; per-device behavior lives in + the device profile + the run-time hooks. + +Touching the locked invariants (`duration`, `repetitions`, +`interaction_covers_duration`, `time_between_run`, +`profilers.batterymanager.*`) requires updating this template so that **all +~200 generated configs change in lockstep** — never edit the generated +configs in place. + +## Per-device gotchas (mirrored from device profiles) + +These are summarized here so a reader of this README does not have to open the +JSON device profiles to understand the decision tree the matrix runner has to +walk for each device. Source of truth: the corresponding +`device_pixelN.json`. + +### Pixel 3 (Android 12 / API 31) + +- **ABI**: `ro.product.cpu.abilist == arm64-v8a,armeabi-v7a` — accepts both + 64-bit-only and 32-bit-only protected APKs. Most permissive of the three + devices. +- **BatteryManager**: works directly. No `BATTERY_STATS` grant needed. +- **Energy decision rule**: always BatteryManager. +- **Serial reference**: `{{PIXEL3_SERIAL}}` (used in the device profile only). + +### Pixel 6 + +- **ABI**: `ro.product.cpu.abilist == arm64-v8a` — 64-bit-only. Same constraint + as Pixel 9: APKs whose only JNI libs live under `lib/armeabi-v7a/` will + install-fail with `INSTALL_FAILED_NO_MATCHING_ABIS`. +- **BatteryManager**: works; if a `SecurityException: BATTERY_STATS permission` + appears, run + `adb -s {{PIXEL6_SERIAL}} shell pm grant android.permission.BATTERY_STATS` + where `` is the BatteryManager companion APK + (e.g. `com.example.batterymanager_utility`). +- **Energy decision rule**: BatteryManager → if grant denied, sysfs fallback. + +### Pixel 9 (Android 15) + +- **ABI (CRITICAL)**: `ro.product.cpu.abilist == arm64-v8a` only. Bangcle-style + packers that emit only `lib/armeabi-v7a/` JNI libs will + `INSTALL_FAILED_NO_MATCHING_ABIS`. This is an artifact-side mismatch, not a + signing or adb bug — never weaken the protection to force install. Either + re-pack with `arm64-v8a`/fat-ABI shells, or mark that + `(app, packed-variant, Pixel 9)` cell as **not supported** in + `final_dataset/`. +- **BatteryManager (CRITICAL)**: companion app raises + `SecurityException: BATTERY_STATS permission` on Android 15 unless granted + explicitly: + `adb -s {{PIXEL9_SERIAL}} shell pm grant android.permission.BATTERY_STATS`. + If the grant is rejected by Android 15's privileged-permission policy, + switch this device to **sysfs sampling** for that run and record it as + BatteryManager-blocked in the run log. Never silently skip Pixel 9 from the + matrix. +- **Energy decision rule**: BatteryManager (after grant) → sysfs fallback if + the grant is denied. + +## Files in this folder + +| File | Purpose | +| -------------------------- | ------------------------------------------------------- | +| `device_pixel3.json` | Pixel 3 (Android 12) device profile + ABI/energy notes. | +| `device_pixel6.json` | Pixel 6 device profile + ABI/energy notes. | +| `device_pixel9.json` | Pixel 9 (Android 15) device profile + ABI/energy notes. | +| `app_variant_2min.json` | Parametric 2-minute experiment template (5 placeholders). | +| `README-templates.md` | This file. | diff --git a/examples/batterymanager/Scripts/README-appium-hooks.md b/examples/batterymanager/Scripts/README-appium-hooks.md new file mode 100644 index 000000000..279271eef --- /dev/null +++ b/examples/batterymanager/Scripts/README-appium-hooks.md @@ -0,0 +1,110 @@ +# Appium `interaction` hooks — quick reference + +This doc covers the AndroidRunner `interaction` hooks for the **black-box Appium harness** +(`appium_android_tests/` package). It is intentionally short; deep documentation lives in: + +- `appium_android_tests/CONVENTIONS.md` — folder layout, per-app `run_workload` contract, + required artifacts, env-var ownership, black-box principle. +- `appium_android_tests/_lib/driver.py` — connect-time env vars (`APPIUM_SERVER_URL`, + `APPIUM_SESSION_CONNECT_TIMEOUT_S`, `APPIUM_SKIP_U2_INSTALL`, `APPIUM_FORCE_U2_INSTALL`, + `APPIUM_ATTACH_ONLY`, `APPIUM_APP_WAIT_DURATION_MS`, `APPIUM_WAIT_FOR_PACKAGE_S`, + `APPIUM_WAIT_MAIN_UI_S`). +- `appium_android_tests/_lib/coverage.py` — `APPIUM_UI_DUMP_DIR`, `APPIUM_UI_DUMP_MAX`, + `APPIUM_SAVE_UI_ON_MISS`. +- `appium_android_tests//scenarios.py` — per-app workload-mode env vars (e.g. + `metronome/scenarios.py` documents `APPIUM_WORKLOAD`, `METRONOME_*_SUBSTR`, + `ESPRESSO_MIRROR_*`, etc.). + +## When to use which hook + +There are three flavors of Appium `interaction` hook in this directory. Pick one per +experiment JSON; they are mutually exclusive on a given run. + +### 1. Generic dispatcher — `interaction_appium.py` (preferred for new apps) + +Use this hook directly and set `APPIUM_APP=` in the experiment JSON's `pre_run` +hook (or in the launcher environment). The dispatcher imports +`appium_android_tests.` and calls its `run_workload(experiment, device)`. + +Example experiment JSON snippet: + +```json +{ + "interaction": "examples/batterymanager/Scripts/interaction_appium.py", + "interaction_covers_duration": true, + "pre_run": "examples/batterymanager/Scripts/before_run_set_app_id.sh" +} +``` + +…where `before_run_set_app_id.sh` does `export APPIUM_APP=avnc` (or whatever app id). + +### 2. Per-app wrapper — `interaction_appium_.py` + +Use this when the launcher cannot easily inject env vars (some AndroidRunner configs only +let you point at a script path) or for backwards compatibility with the existing +`interaction_appium_metronome.py` / `interaction_appium_metronome_espresso_mirror.py` +configs. Copy `interaction_appium_TEMPLATE.py` to `interaction_appium_.py`, +replace `` everywhere, and the wrapper hardcodes `APPIUM_APP` then delegates to +the generic dispatcher. + +The Metronome wrappers (`interaction_appium_metronome*.py`) predate the generic +dispatcher and remain unchanged; they import `appium_android_tests.metronome` directly +and continue to be the canonical path for the existing Metronome configs. + +### 3. Monkey-only — `interaction.py`, `interaction_monkey_only.py` + +Not Appium; documented for completeness. These run `monkey` and read `sysfs` power +samples without UiAutomator2 / scoring artifacts. Use for smoke runs only — they do not +satisfy the strict-UI scoring contract used as the thesis-grade signal. + +## Env-var contract (this hook only) + +Two env vars are owned by the dispatcher itself; everything else is per-app or shared +(see the deep-doc references above and `appium_android_tests/CONVENTIONS.md` § "Env-var +ownership"). + +| Env var | Owner | Purpose | +|---|---|---| +| `APPIUM_APP` | dispatcher (this hook) | App id; required for the generic dispatcher. Ignored by per-app wrappers since they hardcode it. | +| `APPIUM_WORKLOAD` | per-app module | Selects scenario suite within a per-app module. `metronome/scenarios.py` honors `metronome` (default), `espresso_mirror`, `generic`. The dispatcher also accepts `APPIUM_WORKLOAD` as a **legacy** fallback for `APPIUM_APP` only when the value is a single non-reserved token (so `APPIUM_WORKLOAD=espresso_mirror` does NOT silently alias to an app id). | + +The dispatcher must NOT shadow shared env vars; it touches only `APPIUM_APP` (read) and +`APPIUM_WORKLOAD` (read, for legacy detection only — never written). + +## Troubleshooting — `appium_status.json` `failure_reason` strings + +Every dispatcher failure path writes `appium_status.json` with one of the following +`failure_reason` strings. AndroidRunner will mark the run as +`crash_anr_status_missing` rather than as a Python traceback so the energy window stays +classifiable. + +| `failure_reason` (prefix) | Most common cause | Fix | +|---|---|---| +| `APPIUM_APP env var missing` | Generic dispatcher referenced from experiment JSON, but no `APPIUM_APP` in the launcher env. | Set `export APPIUM_APP=` in the launcher / `pre_run` hook, OR switch to the per-app wrapper which hardcodes it. | +| `APPIUM_APP env var ambiguous` | `APPIUM_APP` was empty and the legacy `APPIUM_WORKLOAD` fallback returned a reserved token (`espresso_mirror`, `generic`) or a value with `_`. | Set `APPIUM_APP` explicitly to the app id (e.g. `metronome`) and keep `APPIUM_WORKLOAD` for scenario-suite selection. | +| `appium_android_tests package not on sys.path` | Workspace-root walk-up from the script's location did not find `appium_android_tests/__init__.py`, and the hard-coded fallback also failed. | Confirm the script lives under `/android-runner/examples/batterymanager/Scripts/` and the workspace root contains `appium_android_tests/`; otherwise patch `_FALLBACK_ROOT` in `interaction_appium.py`. | +| `per-app module not found: appium_android_tests.` | `APPIUM_APP=` is set but no `appium_android_tests//` folder exists. | Typo in the app id, or the per-app module hasn't been created yet. Add the folder per CONVENTIONS.md. | +| `per-app module has no run_workload: appium_android_tests.` | The `/__init__.py` doesn't re-export `run_workload`. | Add `from .scenarios import run_workload` to `/__init__.py`. | +| `per-app run_workload raised : ` | The per-app workload itself failed (Appium server unreachable, ImportError on the `appium` client, package not installed on device, etc.). | Check the AndroidRunner log for the full traceback, then triage by exception type. `ImportError: No module named 'appium'` → `pip install -r requirements-appium.txt`. `URLError: Connection refused` → start `appium` server. | + +## Adding a new app — 5-step checklist + +1. **Create the per-app module.** Add `appium_android_tests//scenarios.py` + implementing `run_workload(experiment, device)` per `CONVENTIONS.md`. Reuse helpers + from `appium_android_tests._lib` (`build_uiautomator2_options`, + `connect_remote_webdriver`, `await_app_ready`, `record_numeric_edit_observed`, + `start_workload_thread_and_block`, `write_appium_status`, etc.) — do NOT re-import + `appium` directly outside the workload thread. +2. **Re-export `run_workload`.** In `appium_android_tests//__init__.py`, add + `from .scenarios import run_workload`. +3. **Wire the hook.** Either (a) set `APPIUM_APP=` in the experiment JSON's + `pre_run` hook and reference `interaction_appium.py`, OR (b) copy + `interaction_appium_TEMPLATE.py` to `interaction_appium_.py`, replace `` + with ``, and reference the wrapper. +4. **Set `interaction_covers_duration: true`** in the experiment JSON so + `NativeExperiment` does not double-sleep over the workload window (the per-app + `run_workload` handles its own duration via `start_workload_thread_and_block`). +5. **Smoke-test on Pixel 3** (Android 12) before scaling to Pixel 6 / Pixel 9. Verify + that `appium_workload_coverage.jsonl`, `_scenario_report.{json,txt}`, and + `appium_status.json` land under `paths.OUTPUT_DIR` and that + `appium_status.json.failure_reason` is `null` on success. diff --git a/examples/batterymanager/Scripts/_lib_apk_meta.py b/examples/batterymanager/Scripts/_lib_apk_meta.py new file mode 100644 index 000000000..c9a8b8681 --- /dev/null +++ b/examples/batterymanager/Scripts/_lib_apk_meta.py @@ -0,0 +1,139 @@ +"""APK provenance writer for the (E0.T4) tracking matrix pipeline. + +Produces ``apk_meta.json`` next to a run's output artifacts so the +``after_experiment`` hook (``update_tracking_matrix.py``) can populate +``apk_path`` / ``apk_sha256`` / ``apk_storage`` without needing to be told +the APK path again on the CLI. + +This module is intentionally minimal: + +* standard library only (``hashlib`` / ``json`` / ``os`` / ``sys``); +* never raises -- a failure here MUST NOT abort the experiment lifecycle; +* schema is the subset of fields ``update_tracking_matrix.py`` accepts + (see ``_resolve_apk_provenance`` and ``_find_apk_meta`` there). + +The helper drops the file under the per-(device, subject) data directory +that AndroidRunner exposes as ``paths.OUTPUT_DIR`` at ``before_run`` time. +``_find_apk_meta`` already searches that location via its +``data/*/*/apk_meta.json`` glob, so a single drop is enough; we fall back +to the run-level directory if the per-run directory cannot be derived. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import os.path as op +import sys + + +_CHUNK_BYTES = 64 * 1024 # streaming SHA-256 chunk size + +_FALLBACK_NOTE = "before_run_write_apk_meta" + + +def _log_stderr(msg): + try: + sys.stderr.write("%s: %s\n" % (_FALLBACK_NOTE, msg)) + except Exception: + pass + + +def _log_stdout(msg): + try: + sys.stdout.write("%s: %s\n" % (_FALLBACK_NOTE, msg)) + except Exception: + pass + + +def _sha256_file(path): + """Streaming SHA-256 of ``path``. Returns ``None`` on any read error.""" + h = hashlib.sha256() + try: + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(_CHUNK_BYTES), b""): + h.update(chunk) + except OSError as exc: + _log_stderr("sha256 read failed for %s: %s" % (path, exc)) + return None + return h.hexdigest() + + +def _ensure_dir(path): + try: + os.makedirs(path) + except OSError: + pass + + +def write_apk_meta(run_output_dir, apk_path): + """Drop ``apk_meta.json`` in ``run_output_dir`` for this APK. + + Parameters + ---------- + run_output_dir : str + Directory where the matrix updater can find the file. The most + useful value is ``paths.OUTPUT_DIR`` (per-(device, subject) data + folder) because ``_find_apk_meta`` matches it via its + ``data/*/*/apk_meta.json`` glob. The run-level timestamp directory + also works. + apk_path : str + Path to the APK that was/will be installed for this run. + + Behaviour + --------- + * Computes SHA-256 by streaming the file in 64 KB chunks; if the file + is unreadable, ``apk_sha256`` is written as an empty string and a + ``note`` field records why -- the row will still be populated with + ``apk_path`` and ``apk_storage`` so the gap is visible. + * ``apk_storage`` is fixed to ``"local"`` here -- the migration to + ``gh-release:`` / ``zenodo:`` URIs happens at thesis-submission time + via the post-processing snippet documented in + ``specs/TRACKING_MATRIX.md``. + * Never raises. Any failure is logged to stderr; the row will still + be appended (it just falls back to the existing + ``apk_meta_missing`` notes tag, which is the documented behaviour). + """ + try: + if not run_output_dir: + _log_stderr("no run_output_dir; skipping") + return + if not apk_path: + _log_stderr("no apk_path; skipping") + return + + abs_apk = op.abspath(apk_path) + + meta = { + "apk_path": abs_apk, + "apk_sha256": "", + "apk_storage": "local", + } + + if op.isfile(abs_apk): + digest = _sha256_file(abs_apk) + if digest: + meta["apk_sha256"] = digest + else: + meta["note"] = "apk_sha256_compute_failed" + else: + meta["note"] = "apk_file_not_found_at_hook_time" + + _ensure_dir(run_output_dir) + target = op.join(run_output_dir, "apk_meta.json") + try: + with open(target, "w", encoding="utf-8") as f: + json.dump(meta, f, indent=2, sort_keys=True) + f.write("\n") + except OSError as exc: + _log_stderr("write failed for %s: %s" % (target, exc)) + return + + sha_prefix = (meta["apk_sha256"] or "")[:8] or "" + _log_stdout( + "wrote apk_meta.json (apk_path=%s, sha256=%s...)" + % (abs_apk, sha_prefix) + ) + except Exception as exc: # noqa: BLE001 - hook must never crash the experiment + _log_stderr("unexpected failure: %s: %s" % (type(exc).__name__, exc)) diff --git a/examples/batterymanager/Scripts/_lib_device_state.py b/examples/batterymanager/Scripts/_lib_device_state.py new file mode 100644 index 000000000..a527eabcf --- /dev/null +++ b/examples/batterymanager/Scripts/_lib_device_state.py @@ -0,0 +1,533 @@ +"""Device-state controls + discharge-validity primitives for E0.T8. + +This module is the single source of truth for everything the BatteryManager +pipeline knows about the *physical state* of the phone at the moment a run +starts. Two callers consume it: + + - ``before_experiment_apply_device_state`` → applies the controllable + state (screen brightness lock, best-effort software charge-disable), + runs the discharge-validity check, caches per-device capabilities. + - ``before_run_record_device_state`` → reads everything *back* into + a per-run JSON snapshot (``device_state.json``) so the matrix updater + can later tag rows whose energy reading is suspect. + +Why the split. ``before_experiment`` runs ONCE per experiment and is the +right place to apply state changes. ``before_run`` runs PER RUN and is the +right place to capture a fresh snapshot of `current_now` (because the +charge-controller state can flip between runs — see +``docs/MEASUREMENT_NOISE_SOURCES.md`` § 1b). + +Design contract (mirrors ``before_experiment_grant_battery_stats.py``): + - **Stdlib only.** No third-party imports. + - **Never raises.** Every public function wraps its body in try/except + and returns either a value (with a ``status`` / ``error`` field on + failure) or ``None``. The harness MUST NOT crash inside a hook. + - **Idempotent.** Re-applying state on a re-run is a no-op (set the same + brightness, re-attempt the same dumpsys sequence — both are safe). + - **Best-effort.** ``attempt_disable_charging`` is a *best-effort* + operation: on Pixel 3 / Pixel 9 we already know it does not actually + disable charging at the hardware level (the dumpsys API is accepted + but the charge IC ignores it — see § 1 verdicts table). The function + still runs the sequence, then ``verify_discharging`` reads + ``current_now`` to ground-truth what actually happened. + +Out of scope (per E0.T8 task spec): + - Do NOT automate airplane mode (operator-controlled per the user + constraint clarified in the 2026-05-08 supervisor meeting). + - Do NOT toggle wireless ADB (operator opt-in via RUNBOOK). + - Do NOT modify the BatteryManager plugin itself. +""" + +from __future__ import annotations + +import json +import os +import os.path as op +import sys +import time +from typing import Any, Dict, Optional + + +SETTLE_SECONDS_DEFAULT = 5.0 + +CURRENT_NOW_PATH = "/sys/class/power_supply/battery/current_now" +VOLTAGE_NOW_PATH = "/sys/class/power_supply/battery/voltage_now" + +BATTERYMANAGER_PACKAGE = "com.example.batterymanager_utility" +BATTERY_STATS_PERMISSION = "android.permission.BATTERY_STATS" + +CAPABILITY_CACHE_DIRNAME = ".device_state_capabilities" + +DEFAULT_BRIGHTNESS_VALUE = 128 # mid-range (0..255) +DEFAULT_BRIGHTNESS_MODE = 0 # 0 = manual, 1 = adaptive + +STRICT_ENV_VAR = "MASTEREXP_STRICT_DISCHARGE_CHECK" + + +# --------------------------------------------------------------------------- +# Logging helpers (stderr/stdout only — never raise) +# --------------------------------------------------------------------------- + + +def _log_stdout(msg: str) -> None: + try: + sys.stdout.write(msg + "\n") + sys.stdout.flush() + except Exception: + pass + + +def _log_stderr(msg: str) -> None: + try: + sys.stderr.write(msg + "\n") + sys.stderr.flush() + except Exception: + pass + + +# --------------------------------------------------------------------------- +# Low-level device probes (each returns Optional[T]; None on any failure) +# --------------------------------------------------------------------------- + + +def get_device_serial(device) -> str: + """Best-effort serial extraction. Falls back to ``"unknown-serial"``.""" + try: + return getattr(device, "id", None) or getattr(device, "name", None) or "unknown-serial" + except Exception: + return "unknown-serial" + + +def _device_shell(device, cmd: str) -> Optional[str]: + """Wrap ``device.shell`` so callers never see exceptions. + + Returns the stripped stdout string on success, ``None`` on any failure. + """ + try: + out = device.shell(cmd) + except Exception as ex: + _log_stderr( + "_lib_device_state._device_shell(%r) raised: %s: %s" + % (cmd, type(ex).__name__, ex) + ) + return None + if out is None: + return None + try: + return out.strip() if hasattr(out, "strip") else str(out).strip() + except Exception: + return None + + +def read_current_now_ua(device) -> Optional[int]: + """Read ``/sys/class/power_supply/battery/current_now`` (Pixel = signed µA). + + Returns ``None`` if the file is unreadable or the value is non-numeric. + Negative = discharging (battery → device); positive = charging + (device → battery). Some non-Pixel drivers report mA — this helper + does NOT normalise; the caller should treat values as device-specific + raw and only check sign. + """ + raw = _device_shell(device, "cat %s" % CURRENT_NOW_PATH) + if raw is None: + return None + try: + return int(raw.strip().splitlines()[0]) + except (ValueError, IndexError): + try: + return int(float(raw.strip().splitlines()[0])) + except (ValueError, IndexError): + _log_stderr( + "_lib_device_state.read_current_now_ua: non-numeric value %r" % raw + ) + return None + + +def read_voltage_now_uv(device) -> Optional[int]: + """Read ``/sys/class/power_supply/battery/voltage_now`` (Pixel = µV).""" + raw = _device_shell(device, "cat %s" % VOLTAGE_NOW_PATH) + if raw is None: + return None + try: + return int(raw.strip().splitlines()[0]) + except (ValueError, IndexError): + try: + return int(float(raw.strip().splitlines()[0])) + except (ValueError, IndexError): + return None + + +def read_battery_level_pct(device) -> Optional[int]: + """Parse ``level: NN`` from ``dumpsys battery``.""" + out = _device_shell(device, "dumpsys battery") or "" + for line in out.splitlines(): + s = line.strip() + if s.startswith("level:"): + try: + return int(s.split(":", 1)[1].strip()) + except (ValueError, IndexError): + return None + return None + + +def read_screen_brightness(device) -> Dict[str, Any]: + """Return ``{"value": int|None, "mode": int|None, "mode_label": str|None}``. + + Mode values: 0 = manual, 1 = adaptive (Android system definition). + """ + val_raw = _device_shell(device, "settings get system screen_brightness") + mode_raw = _device_shell(device, "settings get system screen_brightness_mode") + value: Optional[int] = None + mode: Optional[int] = None + if val_raw and val_raw.lower() != "null": + try: + value = int(val_raw) + except ValueError: + value = None + if mode_raw and mode_raw.lower() != "null": + try: + mode = int(mode_raw) + except ValueError: + mode = None + label: Optional[str] + if mode is None: + label = None + elif mode == 0: + label = "manual" + elif mode == 1: + label = "adaptive" + else: + label = "unknown_mode_%s" % mode + return {"value": value, "mode": mode, "mode_label": label} + + +def read_airplane_mode(device) -> Optional[bool]: + """Read-only check of ``settings get global airplane_mode_on``. + + Returns ``True`` / ``False`` / ``None`` (when the setting is absent or + unparseable). The harness does NOT toggle airplane mode — the operator + controls it manually per the 2026-05-08 supervisor-meeting decision. + """ + raw = _device_shell(device, "settings get global airplane_mode_on") + if raw is None or raw.lower() in ("null", ""): + return None + try: + return int(raw) != 0 + except ValueError: + return None + + +def read_third_party_pkg_count(device) -> Optional[int]: + """Count of user-installed (third-party) packages — ``pm list packages -3``. + + Audit aid for "noise source #4 — other foreground/background apps": + a clean test device should have a small, stable count. + """ + out = _device_shell(device, "pm list packages -3") or "" + if not out: + return 0 if out == "" else None + try: + return sum(1 for line in out.splitlines() if line.strip().startswith("package:")) + except Exception: + return None + + +def read_battery_stats_grant(device) -> Optional[bool]: + """Mirror of ``before_experiment_grant_battery_stats._verify_grant``. + + Lives here too so the per-run snapshot doesn't have to import the + grant module (avoids cross-hook coupling in case the grant module is + ever moved or renamed). + """ + out = _device_shell(device, "dumpsys package %s" % BATTERYMANAGER_PACKAGE) + if out is None: + return None + for line in out.splitlines(): + s = line.strip() + if BATTERY_STATS_PERMISSION not in s: + continue + if "granted=true" in s: + return True + if "granted=false" in s: + return False + return None + + +def read_cpu_abilist(device) -> Optional[str]: + """``getprop ro.product.cpu.abilist`` — relevant for Bangcle ABI checks.""" + return _device_shell(device, "getprop ro.product.cpu.abilist") + + +def read_android_release(device) -> Optional[str]: + """``getprop ro.build.version.release`` (e.g. ``"15"``).""" + return _device_shell(device, "getprop ro.build.version.release") + + +# --------------------------------------------------------------------------- +# Charging-disable: best-effort sequence + ground-truth verification +# --------------------------------------------------------------------------- + + +def attempt_disable_charging(device) -> Dict[str, Any]: + """Run the standard ``dumpsys battery`` sequence to *try* to stop charging. + + Returns a dict capturing what was attempted:: + + { + "attempted": True, + "steps": [ + {"cmd": "dumpsys battery unplug", "ok": True / False}, + {"cmd": "dumpsys battery set ac 0", "ok": ...}, + {"cmd": "dumpsys battery set usb 0", "ok": ...}, + {"cmd": "dumpsys battery set wireless 0", "ok": ...}, + {"cmd": "dumpsys battery set status 3", "ok": ...}, # 3 = Discharging + ] + } + + "ok" here only means the command did not raise — it does NOT mean the + charging IC actually obeyed. ``verify_discharging`` is the + ground-truth check that follows. + + Per the § 1 verdicts table, on Pixel 3 + Pixel 9 these commands are + accepted by the API but ignored by the charge controller. We still + run them so we have a documented "we tried" record per device. + """ + sequence = [ + "dumpsys battery unplug", + "dumpsys battery set ac 0", + "dumpsys battery set usb 0", + "dumpsys battery set wireless 0", + "dumpsys battery set status 3", + ] + steps = [] + for cmd in sequence: + try: + device.shell(cmd) + steps.append({"cmd": cmd, "ok": True}) + except Exception as ex: + steps.append({"cmd": cmd, "ok": False, "error": "%s: %s" % (type(ex).__name__, ex)}) + return {"attempted": True, "steps": steps} + + +def verify_discharging(device, settle_s: float = SETTLE_SECONDS_DEFAULT) -> Dict[str, Any]: + """Ground-truth: after ``settle_s`` seconds, read ``current_now`` and classify. + + Verdict semantics:: + + verified_discharge → current_now < 0 (battery is the source) + suspected_supplying → current_now >= 0 (USB / charge IC contributing) + unknown → current_now unreadable + + The dict also carries the raw value so the per-run snapshot can record + the actual µA / mA reading for forensic purposes. + """ + try: + time.sleep(max(0.0, float(settle_s))) + except Exception: + pass + current_ua = read_current_now_ua(device) + if current_ua is None: + return { + "verdict": "unknown", + "current_now_raw": None, + "settle_seconds": settle_s, + "reason": "current_now_unreadable", + } + verdict = "verified_discharge" if current_ua < 0 else "suspected_supplying" + return { + "verdict": verdict, + "current_now_raw": current_ua, + "settle_seconds": settle_s, + "reason": None, + } + + +# --------------------------------------------------------------------------- +# Brightness lock +# --------------------------------------------------------------------------- + + +def apply_brightness_lock( + device, + *, + value: int = DEFAULT_BRIGHTNESS_VALUE, + mode: int = DEFAULT_BRIGHTNESS_MODE, +) -> Dict[str, Any]: + """Set ``screen_brightness_mode`` (0=manual) + ``screen_brightness`` (0..255). + + Returns a dict with what was attempted and what was read back:: + + { + "applied": True, + "set_value": 128, + "set_mode": 0, + "readback": {"value": 128, "mode": 0, "mode_label": "manual"}, + } + + Failures inside the helper are non-fatal — the readback dict will simply + show whatever the device currently reports. + """ + cmds = [ + "settings put system screen_brightness_mode %d" % int(mode), + "settings put system screen_brightness %d" % int(value), + ] + for cmd in cmds: + _device_shell(device, cmd) + return { + "applied": True, + "set_value": int(value), + "set_mode": int(mode), + "readback": read_screen_brightness(device), + } + + +# --------------------------------------------------------------------------- +# Capability cache (per-device JSON, written into Scripts/.device_state_capabilities/) +# --------------------------------------------------------------------------- + + +def capability_cache_dir() -> str: + """Return the directory the capability cache lives in (Scripts/.device_state_capabilities/). + + Created on first use. Living next to the hook scripts keeps the cache + co-located with the code that owns it and survives across runs without + needing the AndroidRunner output dir to be stable. + """ + here = op.dirname(op.abspath(__file__)) + cache_dir = op.join(here, CAPABILITY_CACHE_DIRNAME) + try: + os.makedirs(cache_dir, exist_ok=True) + except Exception as ex: + _log_stderr( + "_lib_device_state.capability_cache_dir: makedirs failed (continuing): %s: %s" + % (type(ex).__name__, ex) + ) + return cache_dir + + +def capability_cache_path(serial: str) -> str: + """Per-device cache file path. Sanitise the serial for filesystem safety.""" + safe = "".join(ch if (ch.isalnum() or ch in "-_") else "_" for ch in (serial or "unknown")) + return op.join(capability_cache_dir(), "%s.json" % safe) + + +def read_capability_cache(serial: str) -> Optional[Dict[str, Any]]: + """Read the cached per-device capability record, or ``None`` on any failure.""" + path = capability_cache_path(serial) + if not op.isfile(path): + return None + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return data if isinstance(data, dict) else None + except Exception as ex: + _log_stderr( + "_lib_device_state.read_capability_cache(%s): %s: %s" + % (path, type(ex).__name__, ex) + ) + return None + + +def write_capability_cache(serial: str, payload: Dict[str, Any]) -> Optional[str]: + """Write the per-device capability record atomically; return the path.""" + path = capability_cache_path(serial) + tmp = path + ".tmp" + try: + with open(tmp, "w", encoding="utf-8") as f: + json.dump(payload, f, indent=2, sort_keys=True, default=str) + f.write("\n") + os.replace(tmp, path) + return path + except Exception as ex: + _log_stderr( + "_lib_device_state.write_capability_cache(%s): %s: %s" + % (path, type(ex).__name__, ex) + ) + return None + + +# --------------------------------------------------------------------------- +# Snapshot assembly (per-run device_state.json schema) +# --------------------------------------------------------------------------- + + +def assemble_device_state_snapshot( + device, + *, + capability_record: Optional[Dict[str, Any]] = None, + settle_s: float = SETTLE_SECONDS_DEFAULT, +) -> Dict[str, Any]: + """Build the dict that ``before_run_record_device_state`` writes per run. + + Schema (every key always present; value is ``None`` / ``"unknown"`` / + explanatory string when the underlying probe failed):: + + { + "schema_version": "e0t8.v1", + "captured_at": "2026-05-09T12:34:56Z", + "device_serial": "", + "android_release": "12" | "15" | None, + "cpu_abilist": "arm64-v8a,armeabi-v7a" | None, + "battery_level_pct": 87 | None, + "current_now_raw": -488 | None, + "voltage_now_uv": 4391000 | None, + "discharge_verdict": "verified_discharge" | "suspected_supplying" | "unknown", + "discharge_settle_seconds": 5.0, + "screen_brightness": {"value":..., "mode":..., "mode_label":...}, + "airplane_mode_on": True | False | None, + "third_party_pkg_count": 7 | None, + "battery_stats_granted": True | False | None, + "capability_record_serial": "" | None, # cache freshness witness + } + + The dict is JSON-serialisable (only Optional[primitive] / dict / list). + """ + serial = get_device_serial(device) + discharge = verify_discharging(device, settle_s=settle_s) + snapshot: Dict[str, Any] = { + "schema_version": "e0t8.v1", + "captured_at": _iso_utc_now(), + "device_serial": serial, + "android_release": read_android_release(device), + "cpu_abilist": read_cpu_abilist(device), + "battery_level_pct": read_battery_level_pct(device), + "current_now_raw": discharge.get("current_now_raw"), + "voltage_now_uv": read_voltage_now_uv(device), + "discharge_verdict": discharge.get("verdict"), + "discharge_settle_seconds": discharge.get("settle_seconds"), + "screen_brightness": read_screen_brightness(device), + "airplane_mode_on": read_airplane_mode(device), + "third_party_pkg_count": read_third_party_pkg_count(device), + "battery_stats_granted": read_battery_stats_grant(device), + "capability_record_serial": (capability_record or {}).get("device_serial"), + } + return snapshot + + +def _iso_utc_now() -> str: + """ISO-8601 UTC string with second precision; safe on all stdlibs.""" + try: + from datetime import datetime, timezone + return datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + except Exception: + return "" + + +# --------------------------------------------------------------------------- +# Strict-mode helper (env-var lookup) +# --------------------------------------------------------------------------- + + +def strict_mode_enabled() -> bool: + """Return True iff ``MASTEREXP_STRICT_DISCHARGE_CHECK=1`` is set in the env. + + Strict mode is opt-in for the eventual thesis batch — it tells + ``before_experiment_apply_device_state`` to abort the experiment when + discharge cannot be verified, so accidentally-USB-supplied rows never + enter the matrix. Default (env var unset / 0) is lenient: tag the row + in ``notes`` and let dev iteration proceed. + """ + try: + return os.environ.get(STRICT_ENV_VAR, "").strip() == "1" + except Exception: + return False diff --git a/examples/batterymanager/Scripts/after_experiment.py b/examples/batterymanager/Scripts/after_experiment.py index c0c12cd6e..5bbaf5272 100644 --- a/examples/batterymanager/Scripts/after_experiment.py +++ b/examples/batterymanager/Scripts/after_experiment.py @@ -1,3 +1,284 @@ +# noinspection PyUnusedLocal +"""``after_experiment`` hook: append one row to ``specs/tracking_matrix.csv``. + +Android Runner calls this once per device after every run on that device has +finished. We hand off to ``update_tracking_matrix.py`` (stdlib-only, never +raises) via subprocess so a bug in the helper or a missing artifact cannot +crash the experiment lifecycle. + +Identity for the row comes from environment variables set by the experiment +launcher:: + + APPIUM_APP → app key, e.g. ``metronome`` (default if unset) + APPIUM_BUILD_LABEL → variant label (``baseline``, ``r8``, ``allatori``, + ``bangcle`` — heuristic mapping from common synonyms + like ``obfuscated`` / ``packed`` is performed when + the value is not one of the canonical names) + +Device key is normalized from ``device.name`` / ``device.id`` (e.g. ``Pixel 3`` +→ ``pixel3``) so the row matches the canonical ``{pixel3, pixel6, pixel9}`` +enum used elsewhere in the project. + +The Android Runner ``BASE_OUTPUT_DIR`` (i.e. the per-experiment timestamp +folder such as ``output/2026.05.05_235701``) is the canonical run-output +directory. Its basename becomes ``run_id`` — the dedup key for the matrix. +""" + +from __future__ import annotations + +import logging +import os +import os.path as op +import re +import subprocess +import sys + + +_LOG = logging.getLogger("after_experiment.tracking_matrix") + + +_VARIANT_ALIASES = { + "baseline": "baseline", + "unprotected": "baseline", + "stock": "baseline", + "original": "baseline", + "r8": "r8", + "minified": "r8", + "allatori": "allatori", + "obfuscated": "allatori", + "obfuscapk": "allatori", + "bangcle": "bangcle", + "packed": "bangcle", + "shielded": "bangcle", + "protected": "bangcle", +} + + +def _normalize_variant(label): + if not label: + return None + key = re.sub(r"[^a-z0-9]+", "", str(label).lower()) + if key in _VARIANT_ALIASES: + return _VARIANT_ALIASES[key] + for token, mapped in _VARIANT_ALIASES.items(): + if token in key: + return mapped + return str(label).lower() or None + + +def _infer_variant_from_run_output(run_output_dir): + """Infer variant from the per-(device, subject) folder name under data/. + + AndroidRunner names that folder by slugifying the APK path, so an APK + called ``app-protected-signed.apk`` ends up as a subdir whose name + contains ``protected``. We scan that slug for any of the known variant + aliases (``protected`` / ``packed`` / ``bangcle`` / ``r8`` / ``allatori`` + / ``baseline`` / ...). Returns ``None`` if the slug is unhelpful. + """ + if not run_output_dir: + return None + data_dir = op.join(run_output_dir, "data") + if not op.isdir(data_dir): + return None + try: + for device_name in os.listdir(data_dir): + device_path = op.join(data_dir, device_name) + if not op.isdir(device_path): + continue + for subject_slug in os.listdir(device_path): + if not op.isdir(op.join(device_path, subject_slug)): + continue + slug = re.sub(r"[^a-z0-9]+", "", subject_slug.lower()) + # Order matters: check more-specific aliases (bangcle, + # allatori) before more-general ones (packed, obfuscated) + # so e.g. "obfuscated-with-allatori" resolves to allatori. + priority = [ + "bangcle", "allatori", "obfuscapk", "r8", + "shielded", "protected", "packed", "obfuscated", "minified", + "baseline", "unprotected", "stock", "original", + ] + for token in priority: + if token in slug: + return _VARIANT_ALIASES.get(token, token) + except OSError: + return None + return None + + +def _normalize_device(name_or_id): + if not name_or_id: + return "unknown" + key = re.sub(r"[^a-z0-9]+", "", str(name_or_id).lower()) + for canonical in ("pixel3", "pixel6", "pixel9"): + if canonical in key: + return canonical + return key or "unknown" + + +# Known per-app slugs registered under appium_android_tests// (as of 2026-05-12). +# When new per-app modules land, add their slugs here so app-inference from the APK +# basename works out of the box. Order doesn't matter (no aliasing — slugs are exact). +_KNOWN_APP_SLUGS = ( + "metronome", "tipuous", "documenter", "linkhub", "repertoire", + "calculator", "iamspeed", "poetskingdom", "keeprecipe", "anothernotes", + "itsok", "dayswithoutbadhabits", "pdfviewer", +) + + +def _infer_app_from_run_output(run_output_dir): + """Infer app slug from the per-(device, subject) folder name under data/. + + AndroidRunner names that folder by slugifying the APK path. Examples: + - .../tipuous_protected.signed.apk → ...apks-tipuous_protected-signed-apk + - .../com.bobek.metronome_26_protected-v2.1.1.signed.apk → + ...apks-com-bobek-metronome_26_protected-v2-1-1-signed-apk + - .../documenter-universal-debug_protected.signed.apk → + ...apks-documenter-universal-debug_protected-signed-apk + - .../Linkhub_protected.signed.apk → ...apks-linkhub_protected-signed-apk + We scan the slug for any token in `_KNOWN_APP_SLUGS`. Returns None if the + slug is unhelpful — caller should fall back to APPIUM_APP env or "unknown" + (NOT to "metronome", which is just the pilot app, not a sensible default). + + Companion to `_infer_variant_from_run_output` and uses the same walk shape. + """ + if not run_output_dir: + return None + data_dir = op.join(run_output_dir, "data") + if not op.isdir(data_dir): + return None + try: + for device_name in os.listdir(data_dir): + device_path = op.join(data_dir, device_name) + if not op.isdir(device_path): + continue + for subject_slug in os.listdir(device_path): + if not op.isdir(op.join(device_path, subject_slug)): + continue + slug = re.sub(r"[^a-z0-9]+", "", subject_slug.lower()) + # Order matters slightly: longer slugs first so e.g. + # "linkhub" wins over a hypothetical "link" subset, and + # "metronome" matches the v2.1.1-suffixed APK basename + # `com-bobek-metronome_26_protected-v2-1-1-signed-apk`. + for app_slug in sorted(_KNOWN_APP_SLUGS, key=len, reverse=True): + if app_slug in slug: + return app_slug + except OSError: + return None + return None + + +def _resolve_run_output_dir(): + """Prefer ``paths.BASE_OUTPUT_DIR`` (the timestamp folder); fall back to ``OUTPUT_DIR``.""" + try: + import paths as ar_paths # type: ignore + except Exception: + return None + base = getattr(ar_paths, "BASE_OUTPUT_DIR", None) + if base and op.isdir(base): + return base + out = getattr(ar_paths, "OUTPUT_DIR", None) + if out and op.isdir(out): + # OUTPUT_DIR is per-(device,subject); walk up to the timestamp dir. + cur = op.abspath(out) + for _ in range(6): + if op.isdir(op.join(cur, "data")): + return cur + parent = op.dirname(cur) + if parent == cur: + break + cur = parent + return out + return None + + +def _resolve_helper_path(): + return op.join(op.dirname(op.abspath(__file__)), "update_tracking_matrix.py") + + # noinspection PyUnusedLocal def main(device, *args, **kwargs): - pass + try: + run_output_dir = _resolve_run_output_dir() + if not run_output_dir: + _LOG.warning("tracking_matrix: could not resolve run output dir; skipping") + return + + # E1.5.T5 — Post-process the built-in `android` profiler's per-(device, + # subject) CSV NOW (vs in after_run, where the CSV was not yet on disk + # — see the 2026-05-08T19:30 run, where after_run returned 12 s before + # `Profilers:Start final aggregation`). One walk of `data///` + # writes `aux/aux_summary.json` per subject; the matrix updater (called + # by the subprocess below) then reads those summaries and populates + # cpu_avg_pct / cpu_p95_pct / mem_pss_avg_mb / mem_pss_max_mb. + try: + import aux_postprocess + results = aux_postprocess.process_run_output_dir(run_output_dir) + n_processed = sum(1 for r in results if not r.get("error")) + n_disabled = sum(1 for r in results if r.get("error") == "android_profiler_disabled") + if results: + _LOG.info( + "aux_postprocess: walked %d subject dir(s) (processed=%d, profiler_disabled=%d)", + len(results), n_processed, n_disabled, + ) + except Exception as exc: + _LOG.warning( + "aux_postprocess: pre-tracking-matrix step failed (continuing): %s: %s", + type(exc).__name__, exc, + ) + + # App resolution chain (post-2026-05-12 fix), most-trusted first: + # 1. APPIUM_APP env var — set by the per-app interaction wrappers + # (e.g. interaction_appium_tipuous.py does + # `os.environ.setdefault("APPIUM_APP", "tipuous")`) or by the + # generic dispatcher's launcher. + # 2. APK-basename slug — derived from the per-subject output folder + # name (which AndroidRunner slugifies from the APK path). + # 3. "unknown" — explicit. Previously this fell back to "metronome", + # which silently misclassified every non-Metronome run as + # Metronome (10 rows in tracking_matrix.csv had to be backfilled + # after this bug was discovered 2026-05-12). NEVER default to a + # specific app slug — make the misclassification visible instead. + app = ( + os.environ.get("APPIUM_APP") + or _infer_app_from_run_output(run_output_dir) + or "unknown" + ) + # Variant resolution, in order of trust: + # 1. APPIUM_BUILD_LABEL env var (explicit, set by the experiment launcher) + # 2. Scan the per-subject output folder name (slug of the APK path) + # 3. Fallback: "baseline" + # This avoids the silent-misclassification bug where a packed APK + # was being recorded as variant=baseline because nothing set the env. + variant = ( + _normalize_variant(os.environ.get("APPIUM_BUILD_LABEL")) + or _infer_variant_from_run_output(run_output_dir) + or "baseline" + ) + device_key = _normalize_device(getattr(device, "name", None) or getattr(device, "id", None)) + + helper = _resolve_helper_path() + if not op.isfile(helper): + _LOG.warning("tracking_matrix: helper not found at %s; skipping", helper) + return + + cmd = [ + sys.executable or "python3", + helper, + "--app", app, + "--variant", variant, + "--device", device_key, + "--run-output-dir", run_output_dir, + ] + _LOG.info("tracking_matrix: invoking %s", " ".join(cmd)) + # Best-effort: the helper itself never raises, but guard the subprocess + # call too in case Python is missing or PATH is broken. + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode != 0: + _LOG.warning( + "tracking_matrix: helper exited %s\nstdout=%s\nstderr=%s", + result.returncode, result.stdout, result.stderr, + ) + elif result.stderr: + _LOG.info("tracking_matrix: helper stderr: %s", result.stderr.strip()) + except Exception as ex: # noqa: BLE001 - hook must never crash the experiment + _LOG.warning("tracking_matrix: update failed (%s: %s)", type(ex).__name__, ex) diff --git a/examples/batterymanager/Scripts/after_launch.py b/examples/batterymanager/Scripts/after_launch.py index d8cbb80b4..c1d71c35d 100644 --- a/examples/batterymanager/Scripts/after_launch.py +++ b/examples/batterymanager/Scripts/after_launch.py @@ -1,3 +1,144 @@ +# noinspection PyUnusedLocal,PyUnusedLocal +"""``after_launch`` hook — last step before Profilers start. + +Original responsibility (preserved): debug-print the args AndroidRunner passes. + +Added 2026-05-15: **aggressive auto-dismiss for the Android 16 ``PageSizeMismatchDialog``** +("App isn't 16 KB compatible"). This dialog appears for: + +1. The BatteryManager Utility companion (``com.example.batterymanager_utility``) — fires once + per process launch when AndroidRunner starts the utility for energy sampling. + +2. The subject-app's MainActivity — fires every time monkey starts a freshly-installed + debug-built app whose native libs aren't 16-KB-aligned. **Every AndroidRunner run does a + fresh install**, so the "Don't Show Again" persistent flag is reset each time — the + dialog WILL re-fire on every run unless we dismiss it after each install. + +The dismiss is **polling-based** (not one-shot) because: +- monkey returns before the activity is fully resumed, so the dialog can appear up to ~3s + after monkey "returns success" +- The dialog can fire AGAIN if the system reloads the activity stack (Compose / CameraX + cold-init can race with the warning system) + +We poll for up to 10 seconds. Each iteration: uiautomator dump → look for "Don't Show +Again" → tap if found. Loop continues until either: (a) we've dismissed once AND the +dialog isn't visible for one consecutive iteration, or (b) the 10s budget expires. + +Best-effort: failures are logged + swallowed (this hook must never crash the run). The +per-app Appium harness also dismisses inside its session as a defense-in-depth layer. + +Full background: ``docs/BANGCLE_PIXEL9_ABI_COMPATIBILITY.md`` § "Adjacent finding — +Android 16 PageSizeMismatchDialog". +""" + +import re +import sys +import time + + +_DSA_PATTERN = re.compile( + r']*text="Don\'t Show Again"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"' +) + + +def _dump_and_extract_dsa_bounds(device) -> tuple[int, int, int, int] | None: + """Dump the UI and extract the 'Don't Show Again' button bounds if visible. Returns + (x1,y1,x2,y2) or None. Best-effort — swallows all exceptions.""" + dump_path = "/sdcard/after_launch_uidump.xml" + xml = "" + try: + device.shell("uiautomator dump %s" % dump_path) + except Exception as exc: + # AndroidRunner's wrapper can raise AdbError on uiautomator's "UI hierchary dumped" + # stderr noise — the file is still written. Stash exception body too in case it + # contains the XML. + xml = str(exc) or xml + try: + out = device.shell("cat %s" % dump_path) or "" + xml = out if out else xml + except Exception as exc: + # Large XML may be wrapped into an AdbError on large stdout — extract from msg. + xml = str(exc) or xml + if "Don't Show Again" not in xml: + return None + m = _DSA_PATTERN.search(xml) + if m is None: + return None + x1, y1, x2, y2 = map(int, m.groups()) + return x1, y1, x2, y2 + + +def _aggressive_dismiss_page_size_dialog(device, total_budget_s: float = 10.0) -> int: + """Poll for the PageSizeMismatchDialog for up to ``total_budget_s`` seconds, tapping + "Don't Show Again" whenever it appears. Returns the number of times the dialog was + dismissed (typically 1 if encountered, 0 if not present). + """ + dismissed = 0 + deadline = time.monotonic() + total_budget_s + consecutive_misses = 0 + while time.monotonic() < deadline: + bounds = None + try: + bounds = _dump_and_extract_dsa_bounds(device) + except Exception: + pass + if bounds is None: + consecutive_misses += 1 + # Exit early if we've successfully dismissed at least once AND the dialog has + # been gone for 2 consecutive polls (= reliably dismissed). + if dismissed >= 1 and consecutive_misses >= 2: + break + time.sleep(0.5) + continue + # Tap the center of the "Don't Show Again" button + x1, y1, x2, y2 = bounds + cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 + try: + device.shell("input tap %d %d" % (cx, cy)) + dismissed += 1 + consecutive_misses = 0 + try: + sys.stdout.write( + "after_launch: dismissed PageSizeMismatchDialog " + "(#%d) via 'Don't Show Again' tap at (%d,%d)\n" + % (dismissed, cx, cy) + ) + except Exception: + pass + except Exception as exc: + try: + sys.stderr.write( + "after_launch: input tap on 'Don't Show Again' failed (continuing): " + "%s: %s\n" % (type(exc).__name__, exc) + ) + except Exception: + pass + # Short settle before re-checking + time.sleep(0.5) + return dismissed + + # noinspection PyUnusedLocal,PyUnusedLocal def main(device, *args, **kwargs): - pass + print("AFTER_LAUNCH ARGS:", args) + print("AFTER_LAUNCH KWARGS:", kwargs) + + # NOTE: We intentionally do NOT start Monkey from this hook. + # On NativeExperiment runs, Android Runner doesn't pass the subject package here, + # and device.current_activity() can point to the BatteryManager utility screen. + # Monkey is started from Scripts/interaction.py where we can access experiment.package. + + # Aggressive polling dismiss for the Android 16 PageSizeMismatchDialog. Critical for + # Bangcle-packed APKs because their libSecShell.so isn't 16-KB-aligned, and AndroidRunner's + # per-run fresh install resets the "Don't Show Again" persistent flag every time. Without + # this, the dialog blocks monkey from successfully foregrounding the subject app, and the + # Android profiler errors with "No process found" when it tries to dump meminfo. + n_dismissed = _aggressive_dismiss_page_size_dialog(device, total_budget_s=10.0) + if n_dismissed > 0: + try: + sys.stdout.write( + "after_launch: PageSizeMismatchDialog dismiss cycle complete " + "(%d tap(s) fired)\n" % n_dismissed + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/after_run.py b/examples/batterymanager/Scripts/after_run.py index 52ef6091e..51e18c9d0 100644 --- a/examples/batterymanager/Scripts/after_run.py +++ b/examples/batterymanager/Scripts/after_run.py @@ -1,3 +1,31 @@ +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + + # noinspection PyUnusedLocal def main(device, *args, **kwargs): device.shell('am force-stop com.example.batterymanager_utility') + + try: + import detect_crash_anr + detect_crash_anr.main(device, *args, **kwargs) + except Exception as exc: + try: + sys.stderr.write( + "after_run: detect_crash_anr hook failed (continuing): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + # E1.5.T5 — `aux_postprocess` USED to be chained here, but the built-in + # `android` profiler's CSV is only written during `aggregate_subject`, + # which fires AFTER `after_run` returns (empirically ~12 s later in the + # 2026-05-08T19:30 run). Calling the post-processor here read no CSV and + # wrote no aux_summary.json. Moved to `after_experiment.py` where every + # per-subject CSV is on disk by the time we walk the data tree. diff --git a/examples/batterymanager/Scripts/aux_postprocess.py b/examples/batterymanager/Scripts/aux_postprocess.py new file mode 100644 index 000000000..4e8583bea --- /dev/null +++ b/examples/batterymanager/Scripts/aux_postprocess.py @@ -0,0 +1,421 @@ +"""E1.5.T5 - Post-process Android Runner's built-in `android` profiler CSV. + +Android Runner ships a CPU + memory profiler at `Plugins/android/Android.py` +(documented in the A-Mobile 2020 paper §3.4) that, when enabled in a config +via:: + + "profilers": { + "android": { "sample_interval": 1000, "data_points": ["cpu", "mem"] } + } + +writes a per-(device, run) CSV to:: + + /data///android/_.csv + +with columns ``datetime, cpu, mem`` (extra columns appear if more +data_points are configured). The plugin already produces an `Aggregated.csv` +(arithmetic mean per data point) at subject-aggregation time, but the mean +alone is not enough for the thesis: we also need p50, p95, and max so the +tracking matrix can answer "is the energy delta explainable by a CPU / +memory work delta?". + +This module is a thin post-processor: + + - finds the most recent `android/_.csv` for the run + - parses each numeric column robustly (tolerates the built-in's quirks + like the ``TOTAL: 35`` prefix on the cpu column on some devices) + - writes ``aux/aux_summary.json`` with avg/p50/p95/max for each column + - exposes a CLI for back-fill against an existing run dir + +The hook is wired into `after_run.py` AFTER `detect_crash_anr` so it never +runs in time-critical code paths. It is best-effort: any failure is logged +to stderr and the experiment continues. + +Stdlib-only. Designed to coexist with pre-`android`-profiler runs (just +returns ``None`` and writes no summary). +""" + +from __future__ import annotations + +import argparse +import csv +import glob +import json +import os +import os.path as op +import re +import sys +from typing import Optional + + +_AUX_SUMMARY_FILENAME = "aux_summary.json" +# The built-in plugin's get_cpu_usage returns a string like "TOTAL: 35.0" +# on some devices (it does `shell_result.split('%')[0]` without trimming the +# leading "TOTAL: " token). We accept both shapes. +_TRAILING_NUMBER_RE = re.compile(r"(-?\d+(?:\.\d+)?)\s*$") + + +def _resolve_run_output_dir() -> Optional[str]: + try: + import paths as ar_paths # type: ignore + except Exception: + return None + out = getattr(ar_paths, "OUTPUT_DIR", None) + if out and op.isdir(out): + return out + base = getattr(ar_paths, "BASE_OUTPUT_DIR", None) + if base and op.isdir(base): + return base + return None + + +def _find_android_csv(per_subject_dir: str) -> Optional[str]: + """Return the newest ``android/_.csv`` under the per-subject dir. + + The built-in plugin writes ONE CSV per call to ``collect_results``, named + ``_.csv`` (see + ``Plugins/android/Android.py:collect_results``). On a re-run the dir may + contain several; we pick the most recent by mtime. + """ + android_dir = op.join(per_subject_dir, "android") + if not op.isdir(android_dir): + return None + candidates = [ + op.join(android_dir, f) + for f in os.listdir(android_dir) + if f.endswith(".csv") and f.lower() != "aggregated.csv" + ] + if not candidates: + return None + candidates.sort(key=lambda p: op.getmtime(p) if op.exists(p) else 0, reverse=True) + return candidates[0] + + +def _coerce_float(raw: Optional[str]) -> Optional[float]: + """Best-effort float coercion that tolerates the built-in plugin's quirks. + + The CPU column on some devices comes back as ``"TOTAL: 35.0"`` because + ``Plugins/android/Android.py:get_cpu_usage`` does + ``shell_result.split('%')[0]`` without trimming the leading "TOTAL: ". + The memory column is always an integer KB string. We just pull the + last numeric token from the cell and cast. + """ + if raw is None: + return None + s = str(raw).strip() + if not s: + return None + try: + return float(s) + except (TypeError, ValueError): + pass + m = _TRAILING_NUMBER_RE.search(s) + if not m: + return None + try: + return float(m.group(1)) + except (TypeError, ValueError): + return None + + +def _percentile(values: list, pct: float) -> Optional[float]: + """Linear-interpolated percentile (0 <= pct <= 100). None on empty input.""" + if not values: + return None + if len(values) == 1: + return float(values[0]) + sorted_vals = sorted(values) + rank = (pct / 100.0) * (len(sorted_vals) - 1) + lo = int(rank) + hi = min(lo + 1, len(sorted_vals) - 1) + frac = rank - lo + return float(sorted_vals[lo] + (sorted_vals[hi] - sorted_vals[lo]) * frac) + + +def _basic_stats(values: list) -> dict: + if not values: + return {"avg": None, "p50": None, "p95": None, "max": None, "min": None, "n": 0} + fs = [float(v) for v in values if v is not None] + if not fs: + return {"avg": None, "p50": None, "p95": None, "max": None, "min": None, "n": 0} + return { + "avg": sum(fs) / len(fs), + "p50": _percentile(fs, 50.0), + "p95": _percentile(fs, 95.0), + "max": max(fs), + "min": min(fs), + "n": len(fs), + } + + +def aggregate_android_csv(csv_path: str) -> dict: + """Compute avg/p50/p95/max for every NON-datetime column in ``csv_path``. + + Returns ``{"csv_path": ..., "samples": N, "columns": {col: stats, ...}}``. + On any failure returns ``{"csv_path": csv_path, "samples": 0, "columns": {}, + "error": ""}``. + """ + if not csv_path or not op.isfile(csv_path): + return {"csv_path": csv_path, "samples": 0, "columns": {}, "error": "csv_missing"} + + try: + with open(csv_path, "r", encoding="utf-8", newline="") as f: + reader = csv.reader(f) + header = next(reader, None) + if not header: + return {"csv_path": csv_path, "samples": 0, "columns": {}, "error": "empty_csv"} + data_columns = [c for c in header if c.lower() != "datetime"] + buckets: dict = {c: [] for c in data_columns} + sample_count = 0 + for row in reader: + if not row: + continue + sample_count += 1 + row_map = dict(zip(header, row)) + for col in data_columns: + v = _coerce_float(row_map.get(col)) + if v is not None: + buckets[col].append(v) + return { + "csv_path": csv_path, + "samples": sample_count, + "columns": {c: _basic_stats(buckets[c]) for c in data_columns}, + } + except Exception as ex: + return { + "csv_path": csv_path, + "samples": 0, + "columns": {}, + "error": "%s: %s" % (type(ex).__name__, ex), + } + + +def write_summary(per_subject_dir: str, payload: dict) -> Optional[str]: + """Write ``payload`` as ``aux/aux_summary.json`` under the per-subject dir. + + Returns the absolute path on success, ``None`` on any failure. + """ + try: + aux_dir = op.join(per_subject_dir, "aux") + os.makedirs(aux_dir, exist_ok=True) + path = op.join(aux_dir, _AUX_SUMMARY_FILENAME) + tmp = path + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(payload, f, indent=2, sort_keys=True, default=str) + os.replace(tmp, path) + return path + except Exception as ex: + try: + sys.stderr.write( + "aux_postprocess.write_summary(%s): %s: %s\n" + % (per_subject_dir, type(ex).__name__, ex) + ) + except Exception: + pass + return None + + +def process_per_subject_dir(per_subject_dir: str) -> dict: + """Find the built-in plugin's CSV under ``per_subject_dir`` and aggregate it. + + Returns the same payload that gets written to ``aux/aux_summary.json``. + When the `android` profiler block is not enabled in the experiment config + (so the CSV does not exist), returns a payload with + ``"error": "android_profiler_disabled"`` and writes nothing. + """ + csv_path = _find_android_csv(per_subject_dir) + if not csv_path: + return { + "csv_path": None, + "samples": 0, + "columns": {}, + "error": "android_profiler_disabled", + } + payload = aggregate_android_csv(csv_path) + written_at = write_summary(per_subject_dir, payload) + payload["written_to"] = written_at + return payload + + +def process_run_output_dir(run_output_dir: str) -> list: + """Walk ``/data///`` and process each subject dir. + + This is the right entrypoint when called from ``after_experiment``, because + AndroidRunner's profiler aggregation (which writes the per-(device, subject) + `android/_.csv`) only completes after all per-run hooks have + fired. By the time ``after_experiment`` runs, every subject dir has its + profiler CSV on disk and we can write one ``aux/aux_summary.json`` per + subject in a single pass. + + Returns a list of payloads (one per per-subject dir that contained an + android profiler CSV). Returns an empty list when ``data/`` is missing or + when no subject dir had a CSV. + """ + results: list = [] + if not run_output_dir: + return results + data_dir = op.join(run_output_dir, "data") + if not op.isdir(data_dir): + return results + try: + device_dirs = sorted( + op.join(data_dir, d) for d in os.listdir(data_dir) + if op.isdir(op.join(data_dir, d)) + ) + except OSError as ex: + try: + sys.stderr.write( + "aux_postprocess.process_run_output_dir: listdir(%s) failed: %s: %s\n" + % (data_dir, type(ex).__name__, ex) + ) + except Exception: + pass + return results + for device_dir in device_dirs: + try: + subject_dirs = sorted( + op.join(device_dir, s) for s in os.listdir(device_dir) + if op.isdir(op.join(device_dir, s)) + ) + except OSError: + continue + for subject_dir in subject_dirs: + payload = process_per_subject_dir(subject_dir) + payload["per_subject_dir"] = subject_dir + results.append(payload) + return results + + +# noinspection PyUnusedLocal +def main(device, *args, **kwargs): + """AndroidRunner-style hook. Best-effort - never raises into the experiment loop. + + Detects which lifecycle slot is calling us: + + - ``after_experiment`` (per-device, AFTER profiler aggregation): walk + every per-subject dir under ``BASE_OUTPUT_DIR/data///`` + and write one ``aux/aux_summary.json`` per subject. This is the + production path — the per-run android CSV is only on disk after the + profiler's ``aggregate_subject`` completes (see 2026-05-08T19:30 run + for the empirical timing: after_run returned 12 s before the CSV + landed, which is why we moved this hook out of after_run). + + - ``after_run`` (legacy / back-compat): still works if the CSV happens + to be on disk by the time after_run fires (rare on most devices). + Falls through to the per-subject path. + """ + try: + per_subject_dir = _resolve_run_output_dir() + if not per_subject_dir: + sys.stderr.write( + "aux_postprocess: cannot resolve per-subject output dir; skipping\n" + ) + return + + # Heuristic: if the resolved dir CONTAINS a `data/` subdir, we are in + # the after_experiment slot (BASE_OUTPUT_DIR). Process all subjects. + # If it IS a subject dir (has an `android/` sibling), process just it. + run_root_candidate = per_subject_dir + if op.isdir(op.join(run_root_candidate, "data")): + results = process_run_output_dir(run_root_candidate) + if not results: + sys.stdout.write( + "aux_postprocess: no per-subject dirs found under %s/data — " + "android profiler may be disabled or experiment had no runs\n" + % run_root_candidate + ) + sys.stdout.flush() + return + n_processed = sum(1 for r in results if not r.get("error")) + n_disabled = sum(1 for r in results if r.get("error") == "android_profiler_disabled") + sys.stdout.write( + "aux_postprocess: walked %s subject dir(s) under %s " + "(processed=%s, profiler_disabled=%s)\n" + % (len(results), run_root_candidate, n_processed, n_disabled) + ) + sys.stdout.flush() + return + + payload = process_per_subject_dir(per_subject_dir) + if payload.get("error") == "android_profiler_disabled": + sys.stdout.write( + "aux_postprocess: built-in `android` profiler not enabled for this run " + "(%s); aux columns in tracking matrix will be empty\n" % per_subject_dir + ) + sys.stdout.flush() + return + cols = payload.get("columns") or {} + sys.stdout.write( + "aux_postprocess: aggregated %s samples across %s data point(s) " + "(written to %s)\n" + % (payload.get("samples", 0), len(cols), payload.get("written_to") or "") + ) + sys.stdout.flush() + except Exception as ex: + try: + sys.stderr.write( + "aux_postprocess.main: unexpected error (continuing): %s: %s\n" + % (type(ex).__name__, ex) + ) + except Exception: + pass + + +def cli(): + """CLI for back-filling existing run dirs. + + Usage:: + + python3 aux_postprocess.py --run-output-dir /path/to/output/2026.05.08_174322 + + The post-processor walks the run dir for ``data///android/`` + sub-trees and writes one ``aux/aux_summary.json`` per per-subject dir found. + """ + parser = argparse.ArgumentParser( + description=( + "Post-process Android Runner's built-in `android` profiler CSV " + "into aux_summary.json (avg/p50/p95/max per column) for the " + "tracking matrix." + ), + ) + parser.add_argument( + "--run-output-dir", + required=True, + help=( + "Either a per-subject dir (containing `android/`) or the " + "top-level run dir (containing `data///android/`). " + "Both are accepted." + ), + ) + args = parser.parse_args() + + root = op.abspath(args.run_output_dir) + if op.isdir(op.join(root, "android")): + targets = [root] + else: + targets = sorted({ + op.dirname(p) + for p in glob.glob(op.join(root, "data", "*", "*", "android")) + if op.isdir(p) + }) + if not targets: + sys.stderr.write( + "aux_postprocess.cli: no `android/` sub-tree under %s\n" % root + ) + return + + for per_subject_dir in targets: + payload = process_per_subject_dir(per_subject_dir) + sys.stdout.write( + "aux_postprocess.cli: %s -> %s (samples=%s, error=%s)\n" + % ( + per_subject_dir, + payload.get("written_to") or "", + payload.get("samples", 0), + payload.get("error") or "ok", + ) + ) + + +if __name__ == "__main__": + cli() diff --git a/examples/batterymanager/Scripts/before_experiment.py b/examples/batterymanager/Scripts/before_experiment.py index 3986f4613..53838148c 100644 --- a/examples/batterymanager/Scripts/before_experiment.py +++ b/examples/batterymanager/Scripts/before_experiment.py @@ -1,7 +1,62 @@ # noinspection PyUnusedLocal +"""Default ``before_experiment`` hook (catch-all for non-Metronome configs). + +Chained hooks (in order; mirrors ``before_experiment_uninstall_metronome.py``): + + 1. **E0.T8 — device-state controls** (``before_experiment_apply_device_state``): + screen brightness lock, best-effort software charge-disable, discharge-validity + check, per-device capability cache. May call ``sys.exit(1)`` only when + ``MASTEREXP_STRICT_DISCHARGE_CHECK=1`` and discharge cannot be verified. + + 2. **E0.T10 — BATTERY_STATS auto-grant** (``before_experiment_grant_battery_stats``): + idempotent ``pm grant`` for the BatteryManager companion. + +This file remains the catch-all default for legacy / non-Metronome experiment +configs that don't have a dedicated per-app uninstall hook. Wiring the chain +here means every experiment that goes through AndroidRunner — regardless of which +``before_experiment`` script is wired in its JSON — gets device-state controls + +USB-supply masking mitigation automatically. + +Both this and ``before_experiment_uninstall_metronome.py`` call the same helpers. +The helpers are idempotent so chaining both would be safe; on a Metronome run, +the chain fires from the uninstall hook first and this module is never invoked. + +Failure handling: any exception is caught and logged to stderr; AndroidRunner +continues. The harness must never crash inside a hook (the only intentional exit +is strict-mode discharge-check abort, which is a documented opt-in). +""" + import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) def main(device, *args, **kwargs): - pass + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment: device-state controls chain failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment: BATTERY_STATS auto-grant chain failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_apply_device_state.py b/examples/batterymanager/Scripts/before_experiment_apply_device_state.py new file mode 100644 index 000000000..a80b9fd31 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_apply_device_state.py @@ -0,0 +1,230 @@ +"""``before_experiment`` helper: apply device-state controls + discharge-validity check. + +Sibling helper to ``before_experiment_grant_battery_stats.py``; invoked from +``before_experiment_uninstall_metronome.py`` (and the catch-all +``before_experiment.py``) via the chain-on-existing-hook pattern. + +What this hook does on every experiment start: + + 1. **Brightness lock** (noise source § 2): force adaptive brightness OFF + and pin ``screen_brightness=128`` so day-vs-night ambient drift cannot + leak ~200 mW of differential into the energy reading. + + 2. **Best-effort software charge-disable** (noise source § 1, § 5): + run the standard ``dumpsys battery unplug`` + ``set ac/usb/wireless 0`` + + ``set status 3`` sequence. On Pixel 3 / Pixel 9 this is known to be + accepted by the Android API but ignored by the charging IC; we still + run it so a per-device cache file documents "we tried" and so devices + where the API *does* work get the right behaviour for free. + + 3. **Discharge-validity ground-truth** (noise source § 1b): wait 5 s then + read ``/sys/class/power_supply/battery/current_now``. Negative = + verified discharge (battery is the source); non-negative = the charge + IC is still supplying / dominating, the energy reading on this run + will be USB-masked. + + 4. **Per-device capability cache**: write a small JSON to + ``Scripts/.device_state_capabilities/.json`` recording + whether software charge-disable actually worked on this device. The + matrix updater can later cite this as the explanation for why a row + is tagged ``energy_invalid_usb_supplying``. + +Strict-mode behaviour (``MASTEREXP_STRICT_DISCHARGE_CHECK=1``): + Default lenient mode (env var unset) → log a warning and continue. + Strict mode (env var = ``"1"``) → write a sentinel file and call + ``sys.exit(1)``. Use strict mode for the eventual thesis batch where + a USB-supplied row would silently corrupt the comparison. + +Out of scope (per E0.T8): + - Do NOT automate airplane mode. + - Do NOT modify the BatteryManager plugin. + - Do NOT install / configure ``hub-ctrl`` (that is Epic 1.6). +""" + +from __future__ import annotations + +import os +import os.path as op +import sys +from typing import Any, Dict, Optional + +_HERE = op.dirname(op.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + + +def _log_stdout(msg: str) -> None: + try: + sys.stdout.write(msg + "\n") + sys.stdout.flush() + except Exception: + pass + + +def _log_stderr(msg: str) -> None: + try: + sys.stderr.write(msg + "\n") + sys.stderr.flush() + except Exception: + pass + + +def _resolve_experiment_root() -> Optional[str]: + """Best-effort: where to drop the strict-mode sentinel file. + + ``paths.BASE_OUTPUT_DIR`` (top-level run dir) is the most useful target + because the operator inspects it after a failed experiment. Falls back + to ``OUTPUT_DIR`` (per-subject) and finally to the Scripts directory. + """ + try: + import paths as ar_paths # type: ignore + except Exception: + return None + base = getattr(ar_paths, "BASE_OUTPUT_DIR", None) + if base and op.isdir(base): + return base + out = getattr(ar_paths, "OUTPUT_DIR", None) + if out and op.isdir(out): + return out + return None + + +def _write_strict_failure_sentinel(snapshot: Dict[str, Any]) -> Optional[str]: + """In strict mode, drop a JSON sentinel describing the discharge failure.""" + try: + import json + target_dir = _resolve_experiment_root() or _HERE + path = op.join(target_dir, "device_state_strict_check_failed.json") + tmp = path + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(snapshot, f, indent=2, sort_keys=True, default=str) + f.write("\n") + os.replace(tmp, path) + return path + except Exception as ex: + _log_stderr( + "before_experiment_apply_device_state: failed to write strict sentinel: %s: %s" + % (type(ex).__name__, ex) + ) + return None + + +def apply_device_state(device) -> Dict[str, Any]: + """Apply brightness + charge-disable + verify discharge; cache capabilities. + + Returns the assembled snapshot dict (same schema as ``device_state.json``) + for callers that want to log or assert on it. The function is independent + of the per-run hook (which builds its own fresh snapshot) — this one runs + once per experiment and is the place where state-changing actions happen. + + Raises ``SystemExit(1)`` only when: + - ``MASTEREXP_STRICT_DISCHARGE_CHECK=1`` is set in the environment, AND + - the post-settle ``current_now`` reading does not classify as + ``verified_discharge``. + + In every other case the function returns normally — failures are logged + to stderr and surface as ``"unknown"`` / ``None`` fields in the snapshot. + """ + import _lib_device_state as ds + + serial = ds.get_device_serial(device) + _log_stdout( + "before_experiment_apply_device_state: starting on %s " + "(strict_mode=%s)" % (serial, "ON" if ds.strict_mode_enabled() else "OFF") + ) + + brightness_result: Optional[Dict[str, Any]] = None + try: + brightness_result = ds.apply_brightness_lock(device) + except Exception as ex: + _log_stderr( + "before_experiment_apply_device_state: brightness lock raised on %s " + "(continuing): %s: %s" % (serial, type(ex).__name__, ex) + ) + + disable_result: Optional[Dict[str, Any]] = None + try: + disable_result = ds.attempt_disable_charging(device) + except Exception as ex: + _log_stderr( + "before_experiment_apply_device_state: attempt_disable_charging raised on %s " + "(continuing): %s: %s" % (serial, type(ex).__name__, ex) + ) + + snapshot = ds.assemble_device_state_snapshot(device) + + cache_payload = { + "device_serial": serial, + "android_release": snapshot.get("android_release"), + "cpu_abilist": snapshot.get("cpu_abilist"), + "last_attempt_disable_charging": disable_result, + "last_brightness_lock": brightness_result, + "last_discharge_verdict": snapshot.get("discharge_verdict"), + "last_current_now_raw": snapshot.get("current_now_raw"), + "last_observed_at": snapshot.get("captured_at"), + "last_battery_level_pct": snapshot.get("battery_level_pct"), + } + written = ds.write_capability_cache(serial, cache_payload) + if written: + _log_stdout( + "before_experiment_apply_device_state: cached capability record at %s" % written + ) + + verdict = snapshot.get("discharge_verdict") or "unknown" + current_raw = snapshot.get("current_now_raw") + if verdict == "verified_discharge": + _log_stdout( + "before_experiment_apply_device_state: discharge verified on %s " + "(current_now=%s) — energy reading on this experiment is trustworthy" + % (serial, current_raw) + ) + elif verdict == "suspected_supplying": + _log_stderr( + "before_experiment_apply_device_state: discharge NOT verified on %s " + "(current_now=%s ≥ 0). USB / charge-IC is still supplying. Per-row " + "energy values WILL be USB-masked. Mitigation: hub-ctrl (Epic 1.6) " + "or wireless ADB (RUNBOOK §5)." % (serial, current_raw) + ) + else: + _log_stderr( + "before_experiment_apply_device_state: discharge UNKNOWN on %s " + "(current_now unreadable). Treating as suspected_supplying for " + "downstream tagging." % serial + ) + + if ds.strict_mode_enabled() and verdict != "verified_discharge": + sentinel_path = _write_strict_failure_sentinel(snapshot) + _log_stderr( + "before_experiment_apply_device_state: STRICT MODE ACTIVE and " + "discharge_verdict=%r on %s — aborting experiment. Sentinel: %s. " + "To proceed anyway: unset %s in your shell." + % ( + verdict, + serial, + sentinel_path or "", + "MASTEREXP_STRICT_DISCHARGE_CHECK", + ) + ) + sys.exit(1) + + return snapshot + + +def main(device, *args, **kwargs): + """Module entrypoint for direct AndroidRunner ``before_experiment`` wiring. + + The catch-all ``before_experiment.py`` and the per-app + ``before_experiment_uninstall_metronome.py`` import this module and call + ``apply_device_state`` directly (chain-on-existing-hook pattern). This + ``main`` exists for symmetry / smoke-testing. + """ + try: + apply_device_state(device) + except SystemExit: + raise + except Exception as ex: + _log_stderr( + "before_experiment_apply_device_state.main: unexpected exception " + "(swallowed; experiment continues): %s: %s" + % (type(ex).__name__, ex) + ) diff --git a/examples/batterymanager/Scripts/before_experiment_grant_battery_stats.py b/examples/batterymanager/Scripts/before_experiment_grant_battery_stats.py new file mode 100644 index 000000000..cf62f8efd --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_grant_battery_stats.py @@ -0,0 +1,161 @@ +"""``before_experiment`` helper: grant ``BATTERY_STATS`` to the BatteryManager companion APK. + +Validated 2026-05-08 cross-permission control on Pixel 3 (Android 12) and Pixel 9 +(Android 15): granting ``com.example.batterymanager_utility`` the +``android.permission.BATTERY_STATS`` runtime permission unmasks the real fuel-gauge +reading on USB-charged devices. Without the grant, the BatteryManager +``BATTERY_PROPERTY_CURRENT_NOW`` API returns the USB-masked value (charging IC supply +minus device draw); with the grant it returns the underlying battery-side current +measurement. Pixel 3 baseline went from 0.011 W (USB-masked) to 0.82 W (real) → 75× +discharge unmasking. See ``docs/MEASUREMENT_NOISE_SOURCES.md`` § 1 for the full +cross-permission table. + +This module is a sibling helper to ``before_experiment_uninstall_metronome.py`` and +is invoked from there via the chain-on-existing-hook pattern (same pattern E0.T4b +introduced in ``before_run.py`` for ``_lib_apk_meta``). Call ``grant_battery_stats(device)`` +before the per-app uninstall step. + +Contract: + - **Idempotent.** ``pm grant`` is a no-op when the permission is already granted. + - **Warn-not-fail.** Every code path is wrapped in try/except; any failure logs + to stderr and returns ``False`` instead of raising into AndroidRunner. + - **Verified.** Reads back ``dumpsys package`` after the grant to confirm + ``granted=true``. The verification result is logged so the + ``after_experiment`` matrix updater can later tag rows where verification + failed (handled separately by E0.T8 ``device_state.json``). + +Out of scope (per E0.T10 task spec): + - Do NOT install the BatteryManager companion APK from this hook. + - Do NOT auto-revoke BATTERY_STATS at the end of the experiment. + - Do NOT touch sysfs. +""" + +from __future__ import annotations + +import sys + +BATTERYMANAGER_PACKAGE = "com.example.batterymanager_utility" +PERMISSION = "android.permission.BATTERY_STATS" + + +def _log_stdout(msg: str) -> None: + try: + sys.stdout.write(msg + "\n") + sys.stdout.flush() + except Exception: + pass + + +def _log_stderr(msg: str) -> None: + try: + sys.stderr.write(msg + "\n") + sys.stderr.flush() + except Exception: + pass + + +def _verify_grant(device) -> bool: + """Return True iff ``dumpsys package`` reports ``BATTERY_STATS: granted=true``. + + Best-effort: any exception or missing output returns False. The caller treats + False as "warn but continue" — the experiment runs anyway and the resulting + rows will simply have a USB-masked energy reading (which the existing noise- + sources doc § 1 catalogues as `RESOLVED-software` only when granted). + """ + try: + out = device.shell("dumpsys package %s" % BATTERYMANAGER_PACKAGE) or "" + except Exception as ex: + _log_stderr( + "before_experiment_grant_battery_stats: verify failed reading dumpsys: %s: %s" + % (type(ex).__name__, ex) + ) + return False + for raw_line in out.splitlines(): + line = raw_line.strip() + if PERMISSION not in line: + continue + if "granted=true" in line: + return True + if "granted=false" in line: + return False + return False + + +def grant_battery_stats(device) -> bool: + """Grant ``BATTERY_STATS`` to the BatteryManager companion APK on ``device``. + + Returns ``True`` if the post-grant verification reports ``granted=true``, + ``False`` otherwise. Never raises. + + The function logs: + - Success → stdout: ``before_experiment_grant_battery_stats: granted ... on `` + - Already-granted → stdout: ``before_experiment_grant_battery_stats: already granted on `` + - Verification failure → stderr: ``before_experiment_grant_battery_stats: grant failed for : `` + - Package missing → stderr: ``before_experiment_grant_battery_stats: package not installed on `` + """ + serial = "unknown-serial" + try: + serial = getattr(device, "id", None) or getattr(device, "name", None) or "unknown-serial" + except Exception: + pass + + try: + installed = False + try: + apps = device.get_app_list() or [] + installed = BATTERYMANAGER_PACKAGE in apps + except Exception as ex: + _log_stderr( + "before_experiment_grant_battery_stats: get_app_list failed on %s " + "(continuing with grant attempt anyway): %s: %s" + % (serial, type(ex).__name__, ex) + ) + installed = True + if not installed: + _log_stderr( + "before_experiment_grant_battery_stats: package %s not installed on %s " + "(install the BatteryManager companion APK during device setup); " + "skipping grant — energy reading will be USB-masked" + % (BATTERYMANAGER_PACKAGE, serial) + ) + return False + + already = _verify_grant(device) + if already: + _log_stdout( + "before_experiment_grant_battery_stats: already granted on %s " + "(no-op; this is the expected path on a re-run)" % serial + ) + return True + + try: + device.shell("pm grant %s %s" % (BATTERYMANAGER_PACKAGE, PERMISSION)) + except Exception as ex: + _log_stderr( + "before_experiment_grant_battery_stats: pm grant raised on %s: %s: %s" + % (serial, type(ex).__name__, ex) + ) + + if _verify_grant(device): + _log_stdout( + "before_experiment_grant_battery_stats: granted %s to %s on %s " + "(USB-supply masking now mitigated for this session)" + % (PERMISSION, BATTERYMANAGER_PACKAGE, serial) + ) + return True + + _log_stderr( + "before_experiment_grant_battery_stats: grant failed for %s: " + "post-grant dumpsys did NOT report granted=true. Energy reading on " + "this run will be USB-masked. Manual fallback: " + "adb -s %s shell pm grant %s %s" + % (serial, serial, BATTERYMANAGER_PACKAGE, PERMISSION) + ) + return False + except Exception as ex: + _log_stderr( + "before_experiment_grant_battery_stats: unexpected exception on %s " + "(swallowed; experiment continues): %s: %s" + % (serial, type(ex).__name__, ex) + ) + return False diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_androidskk.py b/examples/batterymanager/Scripts/before_experiment_uninstall_androidskk.py new file mode 100644 index 000000000..5388405e8 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_androidskk.py @@ -0,0 +1,25 @@ +"""before_experiment for AndroidSKK.""" +import os, sys +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: sys.path.insert(0, _HERE) +PACKAGE = "jp.deadend.noname.skk" + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _ds; _ds.apply_device_state(device) + except SystemExit: raise + except Exception as exc: + try: sys.stderr.write("device_state failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + import before_experiment_grant_battery_stats as _g; _g.grant_battery_stats(device) + except Exception as exc: + try: sys.stderr.write("grant failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + if PACKAGE not in device.get_app_list(): + device.logger.info("%s not installed.", PACKAGE); return + device.uninstall(PACKAGE) + except Exception as exc: + try: sys.stderr.write("uninstall failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_another_notes.py b/examples/batterymanager/Scripts/before_experiment_uninstall_another_notes.py new file mode 100644 index 000000000..c17e710cf --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_another_notes.py @@ -0,0 +1,52 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: device-state + BATTERY_STATS grant + another-notes uninstall.""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.maltaisn.notes.debug" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_another_notes: device-state chain failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_another_notes: BATTERY_STATS grant chain " + "failed (continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass + try: + if PACKAGE not in device.get_app_list(): + device.logger.info("%s not installed; APK will be installed.", PACKAGE) + return + device.logger.info("Uninstalling %s.", PACKAGE) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_another_notes: uninstall failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_avnc.py b/examples/batterymanager/Scripts/before_experiment_uninstall_avnc.py new file mode 100644 index 000000000..9254f1668 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_avnc.py @@ -0,0 +1,68 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS auto-grant + AVNC uninstall. + +Same pattern as the metronome / tipuous / repertoire / linkhub / diaguard uninstall hooks. See +``before_experiment_uninstall_metronome.py`` for the canonical docstring. +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.gaurav.avnc.debug" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_avnc: device-state controls chain " + "failed (continuing with grant + uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_avnc: BATTERY_STATS auto-grant chain " + "failed (continuing with uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info( + "Uninstalling %s so the device only receives the APK from this run's paths.", + PACKAGE, + ) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_avnc: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_calculator.py b/examples/batterymanager/Scripts/before_experiment_uninstall_calculator.py new file mode 100644 index 000000000..fc2b3d32b --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_calculator.py @@ -0,0 +1,71 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS auto-grant + Calculator uninstall. + +Same pattern as the metronome / pdfviewer / horoscapp / crcontainer uninstall hooks. + +PACKAGE = ``com.exponential_groth.calculator`` — applicationId is unsuffixed across debug ++ release (no ``applicationIdSuffix`` in ``app/build.gradle``), so the same value works +for every variant including Bangcle. +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.exponential_groth.calculator" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_calculator: device-state controls chain " + "failed (continuing with grant + uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_calculator: BATTERY_STATS auto-grant chain " + "failed (continuing with uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info( + "Uninstalling %s so the device only receives the APK from this run's paths.", + PACKAGE, + ) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_calculator: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_crcontainer.py b/examples/batterymanager/Scripts/before_experiment_uninstall_crcontainer.py new file mode 100644 index 000000000..7b58a7ce3 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_crcontainer.py @@ -0,0 +1,51 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook for CRcontainer. See ``before_experiment_uninstall_metronome.py``.""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "org.curiouslearning.container" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_crcontainer: device-state chain failed: %s: %s\n" + % (type(exc).__name__, exc)) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_crcontainer: BATTERY_STATS grant failed: %s: %s\n" + % (type(exc).__name__, exc)) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info("%s not installed", PACKAGE) + return + device.logger.info("Uninstalling %s", PACKAGE) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_crcontainer: uninstall failed: %s: %s\n" + % (type(exc).__name__, exc)) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_diaguard.py b/examples/batterymanager/Scripts/before_experiment_uninstall_diaguard.py new file mode 100644 index 000000000..e513459fe --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_diaguard.py @@ -0,0 +1,68 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS auto-grant + Diaguard uninstall. + +Same pattern as the metronome / tipuous / repertoire / linkhub uninstall hooks. See +``before_experiment_uninstall_metronome.py`` for the canonical docstring. +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.faltenreich.diaguard" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_diaguard: device-state controls chain " + "failed (continuing with grant + uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_diaguard: BATTERY_STATS auto-grant chain " + "failed (continuing with uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info( + "Uninstalling %s so the device only receives the APK from this run's paths.", + PACKAGE, + ) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_diaguard: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_documenter.py b/examples/batterymanager/Scripts/before_experiment_uninstall_documenter.py new file mode 100644 index 000000000..d47563994 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_documenter.py @@ -0,0 +1,101 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS auto-grant + Documenter uninstall. + +The original responsibility of this hook (uninstall the previous Documenter install +so the next install is a clean one from the experiment's APK path) is preserved +unchanged. + +Chained hooks (in order): + + 1. **E0.T8 — device-state controls** (``before_experiment_apply_device_state``): + screen brightness lock, best-effort software charge-disable, discharge-validity + check, per-device capability cache. May call ``sys.exit(1)`` if + ``MASTEREXP_STRICT_DISCHARGE_CHECK=1`` and discharge cannot be verified — + this is intentional and only happens in the strict mode used for the eventual + thesis batch. Default lenient mode logs a warning and continues; the per-row + ``notes`` column will carry ``energy_invalid_usb_supplying`` for runs whose + discharge could not be verified. + + 2. **E0.T10 — BATTERY_STATS auto-grant** (``before_experiment_grant_battery_stats``): + idempotent ``pm grant`` for the BatteryManager companion. Required for the + unprivileged-API USB-supply masking mitigation (noise sources § 1). + + 3. Original Documenter uninstall: removes any prior install so the experiment's + declared APK path becomes the install of record. + +Order rationale: physical-world state first (brightness, charge), then permission +state (BATTERY_STATS), then subject state (uninstall). Each step is wrapped so a +failure in one does NOT skip the others. Every failure path logs to stderr and +continues; the harness must never crash inside a hook (the only intentional exit +is the strict-mode discharge-check abort, which is a documented opt-in). + +PACKAGE = ``com.viliussutkus89.documenter.debug`` because the packed APK installed +on Pixel 9 is built from the debug build type which sets +``applicationIdSuffix = ".debug"`` (``app/build.gradle.kts:79-80``). The manifest +package on-device therefore ends in ``.debug`` — using the unsuffixed package +would silently no-op the uninstall and the next install would overlay the +previous one (cross-variant comparisons would be invalid; see +``CONVENTIONS.md`` § 10). +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.viliussutkus89.documenter.debug" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_documenter: device-state controls chain " + "failed (continuing with grant + uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_documenter: BATTERY_STATS auto-grant chain " + "failed (continuing with uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; packed APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info( + "Uninstalling %s so the device only receives the APK from this run's paths (fresh install).", + PACKAGE, + ) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_documenter: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_gallerywall.py b/examples/batterymanager/Scripts/before_experiment_uninstall_gallerywall.py new file mode 100644 index 000000000..c2924527f --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_gallerywall.py @@ -0,0 +1,57 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: device-state + BATTERY_STATS auto-grant + GalleryWall uninstall. + +PACKAGE = ``com.baysoft.gallerywall.dev`` — debug build with ``applicationIdSuffix '.dev'``. +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.baysoft.gallerywall.dev" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_gallerywall: device-state controls chain " + "failed (continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_gallerywall: BATTERY_STATS auto-grant chain " + "failed (continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info("%s not installed; APK will be installed.", PACKAGE) + return + device.logger.info("Uninstalling %s.", PACKAGE) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_gallerywall: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_horoscapp.py b/examples/batterymanager/Scripts/before_experiment_uninstall_horoscapp.py new file mode 100644 index 000000000..c21eda8de --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_horoscapp.py @@ -0,0 +1,75 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS grant + HoroscApp uninstall. + +Same pattern as the metronome / pdfviewer / diaguard uninstall hooks. See +``before_experiment_uninstall_metronome.py`` for the canonical docstring. + +PACKAGE = ``com.angelsoft.horoscapp`` — applicationId is unsuffixed across debug + release. + +Note: CAMERA permission is granted PER RUN (after install) by +``interaction_appium_horoscapp.py`` via ``device.shell("pm grant ... CAMERA")``, NOT here, +because ``pm grant`` requires the app to already be installed and this hook runs BEFORE the +per-run install. +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.angelsoft.horoscapp" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_horoscapp: device-state controls chain " + "failed (continuing with grant + uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_horoscapp: BATTERY_STATS auto-grant chain " + "failed (continuing with uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info( + "Uninstalling %s so the device only receives the APK from this run's paths.", + PACKAGE, + ) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_horoscapp: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_iamspeed.py b/examples/batterymanager/Scripts/before_experiment_uninstall_iamspeed.py new file mode 100644 index 000000000..d28ac0eca --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_iamspeed.py @@ -0,0 +1,61 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: device-state + BATTERY_STATS auto-grant + IamSpeed uninstall. + +PACKAGE = ``com.viliussutkus89.iamspeed.debug`` — debug build has ``applicationIdSuffix +".debug"`` per ``app/build.gradle``. Stub-bangcle pack preserves the same applicationId. +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.viliussutkus89.iamspeed.debug" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_iamspeed: device-state controls chain " + "failed (continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_iamspeed: BATTERY_STATS auto-grant chain " + "failed (continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info("Uninstalling %s.", PACKAGE) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_iamspeed: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_itsok.py b/examples/batterymanager/Scripts/before_experiment_uninstall_itsok.py new file mode 100644 index 000000000..206c0fb88 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_itsok.py @@ -0,0 +1,53 @@ +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS grant + ItsOK uninstall.""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.qubacy.itsok" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_itsok: device-state controls chain failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_itsok: BATTERY_STATS grant failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info("%s not installed; APK will be installed from experiment paths.", PACKAGE) + return + device.logger.info("Uninstalling %s so the device only receives the APK from this run's paths.", PACKAGE) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_itsok: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_keep_recipe.py b/examples/batterymanager/Scripts/before_experiment_uninstall_keep_recipe.py new file mode 100644 index 000000000..1fc32fad7 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_keep_recipe.py @@ -0,0 +1,25 @@ +"""before_experiment for Keep-Recipe.""" +import os, sys +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: sys.path.insert(0, _HERE) +PACKAGE = "com.keeprecipes.android" + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _ds; _ds.apply_device_state(device) + except SystemExit: raise + except Exception as exc: + try: sys.stderr.write("device_state failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + import before_experiment_grant_battery_stats as _g; _g.grant_battery_stats(device) + except Exception as exc: + try: sys.stderr.write("grant failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + if PACKAGE not in device.get_app_list(): + device.logger.info("%s not installed.", PACKAGE); return + device.uninstall(PACKAGE) + except Exception as exc: + try: sys.stderr.write("uninstall failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_keepitup.py b/examples/batterymanager/Scripts/before_experiment_uninstall_keepitup.py new file mode 100644 index 000000000..9c68f3ec2 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_keepitup.py @@ -0,0 +1,25 @@ +"""before_experiment for keepitup.""" +import os, sys +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: sys.path.insert(0, _HERE) +PACKAGE = "net.ibbaa.keepitup" + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _ds; _ds.apply_device_state(device) + except SystemExit: raise + except Exception as exc: + try: sys.stderr.write("device_state failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + import before_experiment_grant_battery_stats as _g; _g.grant_battery_stats(device) + except Exception as exc: + try: sys.stderr.write("grant failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + if PACKAGE not in device.get_app_list(): + device.logger.info("%s not installed.", PACKAGE); return + device.uninstall(PACKAGE) + except Exception as exc: + try: sys.stderr.write("uninstall failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_linkhub.py b/examples/batterymanager/Scripts/before_experiment_uninstall_linkhub.py new file mode 100644 index 000000000..f5370325d --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_linkhub.py @@ -0,0 +1,93 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS auto-grant + LinkHub uninstall. + +The original responsibility of this hook (uninstall the previous LinkHub install +so the next install is a clean one from the experiment's APK path) is preserved +unchanged. + +Chained hooks (in order): + + 1. **E0.T8 — device-state controls** (``before_experiment_apply_device_state``): + screen brightness lock, best-effort software charge-disable, discharge-validity + check, per-device capability cache. May call ``sys.exit(1)`` if + ``MASTEREXP_STRICT_DISCHARGE_CHECK=1`` and discharge cannot be verified — + this is intentional and only happens in the strict mode used for the eventual + thesis batch. Default lenient mode logs a warning and continues; the per-row + ``notes`` column will carry ``energy_invalid_usb_supplying`` for runs whose + discharge could not be verified. + + 2. **E0.T10 — BATTERY_STATS auto-grant** (``before_experiment_grant_battery_stats``): + idempotent ``pm grant`` for the BatteryManager companion. Required for the + unprivileged-API USB-supply masking mitigation (noise sources § 1). + + 3. Original LinkHub uninstall: removes any prior install so the experiment's + declared APK path becomes the install of record. + +Order rationale: physical-world state first (brightness, charge), then permission +state (BATTERY_STATS), then subject state (uninstall). Each step is wrapped so a +failure in one does NOT skip the others. Every failure path logs to stderr and +continues; the harness must never crash inside a hook (the only intentional exit +is the strict-mode discharge-check abort, which is a documented opt-in). +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.amrdeveloper.linkhub" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_linkhub: device-state controls chain " + "failed (continuing with grant + uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_linkhub: BATTERY_STATS auto-grant chain " + "failed (continuing with uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info( + "Uninstalling %s so the device only receives the APK from this run's paths (fresh install).", + PACKAGE, + ) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_linkhub: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_medtimer.py b/examples/batterymanager/Scripts/before_experiment_uninstall_medtimer.py new file mode 100644 index 000000000..6777b0568 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_medtimer.py @@ -0,0 +1,25 @@ +"""before_experiment for MedTimer.""" +import os, sys +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: sys.path.insert(0, _HERE) +PACKAGE = "com.futsch1.medtimer" + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _ds; _ds.apply_device_state(device) + except SystemExit: raise + except Exception as exc: + try: sys.stderr.write("device_state failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + import before_experiment_grant_battery_stats as _g; _g.grant_battery_stats(device) + except Exception as exc: + try: sys.stderr.write("grant failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + if PACKAGE not in device.get_app_list(): + device.logger.info("%s not installed.", PACKAGE); return + device.uninstall(PACKAGE) + except Exception as exc: + try: sys.stderr.write("uninstall failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_metronome.py b/examples/batterymanager/Scripts/before_experiment_uninstall_metronome.py new file mode 100644 index 000000000..3ea2621bd --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_metronome.py @@ -0,0 +1,93 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS auto-grant + Metronome uninstall. + +The original responsibility of this hook (uninstall the previous Metronome install +so the next install is a clean one from the experiment's APK path) is preserved +unchanged. + +Chained hooks (in order): + + 1. **E0.T8 — device-state controls** (``before_experiment_apply_device_state``): + screen brightness lock, best-effort software charge-disable, discharge-validity + check, per-device capability cache. May call ``sys.exit(1)`` if + ``MASTEREXP_STRICT_DISCHARGE_CHECK=1`` and discharge cannot be verified — + this is intentional and only happens in the strict mode used for the eventual + thesis batch. Default lenient mode logs a warning and continues; the per-row + ``notes`` column will carry ``energy_invalid_usb_supplying`` for runs whose + discharge could not be verified. + + 2. **E0.T10 — BATTERY_STATS auto-grant** (``before_experiment_grant_battery_stats``): + idempotent ``pm grant`` for the BatteryManager companion. Required for the + unprivileged-API USB-supply masking mitigation (noise sources § 1). + + 3. Original Metronome uninstall: removes any prior install so the experiment's + declared APK path becomes the install of record. + +Order rationale: physical-world state first (brightness, charge), then permission +state (BATTERY_STATS), then subject state (uninstall). Each step is wrapped so a +failure in one does NOT skip the others. Every failure path logs to stderr and +continues; the harness must never crash inside a hook (the only intentional exit +is the strict-mode discharge-check abort, which is a documented opt-in). +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.bobek.metronome" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_metronome: device-state controls chain " + "failed (continuing with grant + uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_metronome: BATTERY_STATS auto-grant chain " + "failed (continuing with uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; packed APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info( + "Uninstalling %s so the device only receives the APK from this run's paths (fresh install).", + PACKAGE, + ) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_metronome: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_pdfviewer.py b/examples/batterymanager/Scripts/before_experiment_uninstall_pdfviewer.py new file mode 100644 index 000000000..d0c1858be --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_pdfviewer.py @@ -0,0 +1,72 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS auto-grant + Pdf-Viewer uninstall. + +Same pattern as the metronome / tipuous / repertoire / linkhub / diaguard / avnc uninstall hooks. +See ``before_experiment_uninstall_metronome.py`` for the canonical docstring. + +PACKAGE = ``com.rajat.sample.pdfviewer`` — applicationId is unsuffixed across debug + release +(no ``applicationIdSuffix`` in ``app/build.gradle.kts``), so the same value works for every +variant including Bangcle. +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.rajat.sample.pdfviewer" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_pdfviewer: device-state controls chain " + "failed (continuing with grant + uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_pdfviewer: BATTERY_STATS auto-grant chain " + "failed (continuing with uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info( + "Uninstalling %s so the device only receives the APK from this run's paths.", + PACKAGE, + ) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_pdfviewer: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_podaura.py b/examples/batterymanager/Scripts/before_experiment_uninstall_podaura.py new file mode 100644 index 000000000..2596ddfb8 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_podaura.py @@ -0,0 +1,78 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook for PodAura: chain device-state controls + +BATTERY_STATS auto-grant + uninstall + pre-grant runtime perms. + +Same pattern as the other apps' uninstall hooks, plus the extra perms PodAura +needs at runtime (POST_NOTIFICATIONS, READ_MEDIA_* and the special-permission +MANAGE_EXTERNAL_STORAGE via appops). Pre-granting avoids the in-app dialog +flow at S1. +""" + +import os +import subprocess +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.skyd.anivu.debug" + + +def _adb(device, *args): + """Run an `adb` shell command via the device's serial.""" + serial = getattr(device, "id", None) or getattr(device, "serial", None) + cmd = ["adb"] + if serial: + cmd += ["-s", serial] + cmd += list(args) + try: + subprocess.run(cmd, check=False, timeout=10, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except Exception: + pass + + +def _grant_runtime_perms(device): + _adb(device, "shell", "pm", "grant", PACKAGE, "android.permission.POST_NOTIFICATIONS") + _adb(device, "shell", "pm", "grant", PACKAGE, "android.permission.READ_MEDIA_IMAGES") + _adb(device, "shell", "pm", "grant", PACKAGE, "android.permission.READ_MEDIA_VISUAL_USER_SELECTED") + _adb(device, "shell", "appops", "set", PACKAGE, "MANAGE_EXTERNAL_STORAGE", "allow") + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_podaura: device-state chain failed " + "(continuing): %s: %s\n" % (type(exc).__name__, exc)) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_podaura: BATTERY_STATS grant chain " + "failed (continuing): %s: %s\n" % (type(exc).__name__, exc)) + except Exception: + pass + + try: + if PACKAGE in device.get_app_list(): + device.logger.info("Uninstalling %s so the next run installs fresh.", PACKAGE) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_podaura: uninstall step failed " + "(continuing): %s: %s\n" % (type(exc).__name__, exc)) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_poetskingdom.py b/examples/batterymanager/Scripts/before_experiment_uninstall_poetskingdom.py new file mode 100644 index 000000000..291743ff2 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_poetskingdom.py @@ -0,0 +1,25 @@ +"""``before_experiment`` for PoetsKingdom.""" +import os, sys +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: sys.path.insert(0, _HERE) +PACKAGE = "com.wendorochena.poetskingdom" + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _ds; _ds.apply_device_state(device) + except SystemExit: raise + except Exception as exc: + try: sys.stderr.write("device_state chain failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + import before_experiment_grant_battery_stats as _g; _g.grant_battery_stats(device) + except Exception as exc: + try: sys.stderr.write("battery_stats grant failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + if PACKAGE not in device.get_app_list(): + device.logger.info("%s not installed; will install from paths.", PACKAGE); return + device.logger.info("Uninstalling %s.", PACKAGE); device.uninstall(PACKAGE) + except Exception as exc: + try: sys.stderr.write("uninstall step failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_repertoire.py b/examples/batterymanager/Scripts/before_experiment_uninstall_repertoire.py new file mode 100644 index 000000000..39dfb59dd --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_repertoire.py @@ -0,0 +1,99 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS auto-grant + Repertoire uninstall. + +The original responsibility of this hook (uninstall the previous Repertoire +install so the next install is a clean one from the experiment's APK path) is +preserved unchanged. Mirrors the structure of +``before_experiment_uninstall_metronome.py``. + +Chained hooks (in order): + + 1. **E0.T8 — device-state controls** (``before_experiment_apply_device_state``): + screen brightness lock, best-effort software charge-disable, discharge-validity + check, per-device capability cache. May call ``sys.exit(1)`` if + ``MASTEREXP_STRICT_DISCHARGE_CHECK=1`` and discharge cannot be verified — + this is intentional and only happens in the strict mode used for the eventual + thesis batch. Default lenient mode logs a warning and continues; the per-row + ``notes`` column will carry ``energy_invalid_usb_supplying`` for runs whose + discharge could not be verified. + + 2. **E0.T10 — BATTERY_STATS auto-grant** (``before_experiment_grant_battery_stats``): + idempotent ``pm grant`` for the BatteryManager companion. Required for the + unprivileged-API USB-supply masking mitigation (noise sources § 1). + + 3. Original Repertoire uninstall: removes any prior install so the experiment's + declared APK path becomes the install of record. + +Order rationale: physical-world state first (brightness, charge), then permission +state (BATTERY_STATS), then subject state (uninstall). Each step is wrapped so a +failure in one does NOT skip the others. Every failure path logs to stderr and +continues; the harness must never crash inside a hook (the only intentional exit +is the strict-mode discharge-check abort, which is a documented opt-in). + +NOTE: this hook does NOT push the markdown song fixture to ``/sdcard/Download/`` +— that is a one-time per-device setup step the user performs before the first +smoke run. See ``appium_android_tests/repertoire/CALIBRATION.md`` for the +``adb push`` recipe. +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "klalumiere.repertoire" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_repertoire: device-state controls chain " + "failed (continuing with grant + uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_repertoire: BATTERY_STATS auto-grant chain " + "failed (continuing with uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info( + "Uninstalling %s so the device only receives the APK from this run's paths (fresh install).", + PACKAGE, + ) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_repertoire: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_tipuous.py b/examples/batterymanager/Scripts/before_experiment_uninstall_tipuous.py new file mode 100644 index 000000000..000983ae7 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_tipuous.py @@ -0,0 +1,93 @@ +# noinspection PyUnusedLocal +"""``before_experiment`` hook: chain device-state controls + BATTERY_STATS auto-grant + Tipuous uninstall. + +The original responsibility of this hook (uninstall the previous Tipuous install +so the next install is a clean one from the experiment's APK path) is preserved +unchanged. + +Chained hooks (in order): + + 1. **E0.T8 — device-state controls** (``before_experiment_apply_device_state``): + screen brightness lock, best-effort software charge-disable, discharge-validity + check, per-device capability cache. May call ``sys.exit(1)`` if + ``MASTEREXP_STRICT_DISCHARGE_CHECK=1`` and discharge cannot be verified — + this is intentional and only happens in the strict mode used for the eventual + thesis batch. Default lenient mode logs a warning and continues; the per-row + ``notes`` column will carry ``energy_invalid_usb_supplying`` for runs whose + discharge could not be verified. + + 2. **E0.T10 — BATTERY_STATS auto-grant** (``before_experiment_grant_battery_stats``): + idempotent ``pm grant`` for the BatteryManager companion. Required for the + unprivileged-API USB-supply masking mitigation (noise sources § 1). + + 3. Original Tipuous uninstall: removes any prior install so the experiment's + declared APK path becomes the install of record. + +Order rationale: physical-world state first (brightness, charge), then permission +state (BATTERY_STATS), then subject state (uninstall). Each step is wrapped so a +failure in one does NOT skip the others. Every failure path logs to stderr and +continues; the harness must never crash inside a hook (the only intentional exit +is the strict-mode discharge-check abort, which is a documented opt-in). +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "com.tips.tipuous" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _device_state + _device_state.apply_device_state(device) + except SystemExit: + raise + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_tipuous: device-state controls chain " + "failed (continuing with grant + uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + import before_experiment_grant_battery_stats as _grant + _grant.grant_battery_stats(device) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_tipuous: BATTERY_STATS auto-grant chain " + "failed (continuing with uninstall): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + try: + if PACKAGE not in device.get_app_list(): + device.logger.info( + "%s not installed; APK will be installed from experiment paths.", + PACKAGE, + ) + return + device.logger.info( + "Uninstalling %s so the device only receives the APK from this run's paths (fresh install).", + PACKAGE, + ) + device.uninstall(PACKAGE) + except Exception as exc: + try: + sys.stderr.write( + "before_experiment_uninstall_tipuous: uninstall step failed " + "(continuing): {}: {}\n".format(type(exc).__name__, exc) + ) + except Exception: + pass diff --git a/examples/batterymanager/Scripts/before_experiment_uninstall_ytalarm.py b/examples/batterymanager/Scripts/before_experiment_uninstall_ytalarm.py new file mode 100644 index 000000000..d1f3ff610 --- /dev/null +++ b/examples/batterymanager/Scripts/before_experiment_uninstall_ytalarm.py @@ -0,0 +1,30 @@ +# noinspection PyUnusedLocal +"""before_experiment for YtAlarm: device-state + BATTERY_STATS grant + uninstall.""" +import os, sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +PACKAGE = "net.turtton.ytalarm" + + +def main(device, *args, **kwargs): + try: + import before_experiment_apply_device_state as _ds; _ds.apply_device_state(device) + except SystemExit: raise + except Exception as exc: + try: sys.stderr.write("device_state failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + import before_experiment_grant_battery_stats as _g; _g.grant_battery_stats(device) + except Exception as exc: + try: sys.stderr.write("grant failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass + try: + if PACKAGE in device.get_app_list(): + device.logger.info("Uninstalling %s.", PACKAGE) + device.uninstall(PACKAGE) + except Exception as exc: + try: sys.stderr.write("uninstall failed: %s: %s\n" % (type(exc).__name__, exc)) + except Exception: pass diff --git a/examples/batterymanager/Scripts/before_run.py b/examples/batterymanager/Scripts/before_run.py index cb1e6119a..83be09ceb 100644 --- a/examples/batterymanager/Scripts/before_run.py +++ b/examples/batterymanager/Scripts/before_run.py @@ -1,8 +1,109 @@ +"""``before_run`` hook for the BatteryManager experiments. + +In addition to the original ``am start`` of the BatteryManager utility, this +hook now produces ``apk_meta.json`` for the active run so the +``after_experiment`` tracking-matrix updater can populate the +``apk_path`` / ``apk_sha256`` / ``apk_storage`` columns without needing +those values to be passed on the CLI. + +Implementation note: AndroidRunner's ``scripts.before_run`` slot accepts a +single script path, so we extend this existing hook rather than wiring a +new one into every experiment JSON. The chain-on-existing-hook pattern is +the same one ``after_run.py`` already uses for ``detect_crash_anr``. +""" + +import os +import sys import time +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + + +def _resolve_run_output_dir(): + """Best-effort resolution of the directory ``apk_meta.json`` should land in. + + Prefers ``paths.OUTPUT_DIR`` (per-(device, subject) data folder) because + ``update_tracking_matrix.py:_find_apk_meta`` matches that location via + its ``data/*/*/apk_meta.json`` glob. Falls back to ``BASE_OUTPUT_DIR``. + """ + try: + import paths as ar_paths # type: ignore + except Exception: + return None + out = getattr(ar_paths, "OUTPUT_DIR", None) + if out and os.path.isdir(out): + return out + base = getattr(ar_paths, "BASE_OUTPUT_DIR", None) + if base and os.path.isdir(base): + return base + return None + + +def _resolve_apk_path(args, kwargs): + """Pull the active APK path out of whatever AndroidRunner forwarded. + + AndroidRunner calls ``scripts.run('before_run', device, *args, **kwargs)`` + where ``kwargs['current_run']`` is a dict containing ``path`` -- the APK + path for this run (see ``AndroidRunner/Experiment.py``). Some callers + may pass the path as a positional, so we accept both. + """ + current_run = kwargs.get("current_run") if isinstance(kwargs, dict) else None + if isinstance(current_run, dict): + path = current_run.get("path") + if path: + return path + for arg in args or (): + if isinstance(arg, str) and arg.lower().endswith(".apk"): + return arg + return None + # noinspection PyUnusedLocal def main(device, *args, **kwargs): device.shell('am start -n "com.example.batterymanager_utility/com.example.batterymanager_utility.MainActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER') time.sleep(5) + try: + import _lib_apk_meta + run_output_dir = _resolve_run_output_dir() + apk_path = _resolve_apk_path(args, kwargs) + _lib_apk_meta.write_apk_meta(run_output_dir, apk_path) + except Exception as exc: # noqa: BLE001 - hook must never crash the experiment + try: + sys.stderr.write( + "before_run: apk_meta hook failed (continuing): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + # E0.T8 — per-run device_state.json snapshot. Captures battery / + # discharge / brightness / airplane-mode / etc. so the matrix updater + # can tag rows with `energy_invalid_usb_supplying` when the + # charge controller flipped between runs (noise sources § 1b + # "float-charge masking"). Never raises; failure logs to stderr and the + # row simply loses its discharge tag. + try: + import before_run_record_device_state as _device_state_recorder + _device_state_recorder.record_device_state(device, _resolve_run_output_dir()) + except Exception as exc: # noqa: BLE001 - hook must never crash the experiment + try: + sys.stderr.write( + "before_run: device_state.json hook failed (continuing): {}: {}\n".format( + type(exc).__name__, exc + ) + ) + except Exception: + pass + + # E1.5.T1 + T2: CPU + memory sampling is performed by Android Runner's + # built-in `android` profiler plugin (Plugins/android/Android.py); see the + # `profilers.android` block in the experiment JSON. No before_run wiring + # required — the profiler lifecycle is driven by AndroidRunner itself. + # E1.5.T3: per-run logcat capture is provided by the BatteryManager + # plugin's `adb_log` persistency_strategy (already enabled in every + # active config); the resulting `logcat__.txt` is consumed + # automatically by `detect_crash_anr.read_logcat_from_run_dir`. diff --git a/examples/batterymanager/Scripts/before_run_record_device_state.py b/examples/batterymanager/Scripts/before_run_record_device_state.py new file mode 100644 index 000000000..6bccafa48 --- /dev/null +++ b/examples/batterymanager/Scripts/before_run_record_device_state.py @@ -0,0 +1,165 @@ +"""``before_run`` helper: snapshot device state into ``device_state.json``. + +Per-run companion to ``before_experiment_apply_device_state.py``. The +``before_experiment`` hook applied state once at experiment start and +verified discharge once; this hook captures a *fresh* snapshot every run +because the charge-controller state can flip between runs even with the +``BATTERY_STATS`` grant carried over (see noise sources § 1b "float-charge +masking"). + +The resulting ``device_state.json`` lands in ``paths.OUTPUT_DIR`` (i.e. +``/data///device_state.json``) so the +``after_experiment`` matrix updater can read it via the same +``data/*/*/device_state.json`` glob it already uses for ``apk_meta.json``. + +Schema (all 9 fields from the E0.T8 spec):: + + { + "schema_version": "e0t8.v1", + "captured_at": "2026-05-09T12:34:56Z", + "device_serial": "", + "android_release": "12" | "15" | None, + "cpu_abilist": "arm64-v8a,armeabi-v7a" | None, + "battery_level_pct": 87 | None, + "current_now_raw": -488 | None, + "voltage_now_uv": 4391000 | None, + "discharge_verdict": "verified_discharge" | "suspected_supplying" | "unknown", + "discharge_settle_seconds": 5.0, + "screen_brightness": {"value":..., "mode":..., "mode_label":...}, + "airplane_mode_on": True | False | None, + "third_party_pkg_count": 7 | None, + "battery_stats_granted": True | False | None, + "capability_record_serial": "" | None + } + +Contract: + - **Stdlib only.** No third-party imports. + - **Never raises.** Hook must not crash the experiment. + - **No state mutation.** Read-only probes; charging-disable + brightness + are applied by the experiment-level hook, NOT here. +""" + +from __future__ import annotations + +import json +import os +import os.path as op +import sys +from typing import Any, Dict, Optional + +_HERE = op.dirname(op.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + + +_FILENAME = "device_state.json" + + +def _log_stdout(msg: str) -> None: + try: + sys.stdout.write(msg + "\n") + sys.stdout.flush() + except Exception: + pass + + +def _log_stderr(msg: str) -> None: + try: + sys.stderr.write(msg + "\n") + sys.stderr.flush() + except Exception: + pass + + +def _resolve_run_output_dir() -> Optional[str]: + """Mirror of ``before_run._resolve_run_output_dir``. + + Prefers ``paths.OUTPUT_DIR`` (per-(device, subject) data folder) so the + matrix updater finds the file via its ``data/*/*/device_state.json`` glob. + """ + try: + import paths as ar_paths # type: ignore + except Exception: + return None + out = getattr(ar_paths, "OUTPUT_DIR", None) + if out and op.isdir(out): + return out + base = getattr(ar_paths, "BASE_OUTPUT_DIR", None) + if base and op.isdir(base): + return base + return None + + +def record_device_state(device, run_output_dir: Optional[str] = None) -> Optional[str]: + """Build the snapshot dict and write ``device_state.json`` atomically. + + Returns the absolute path written, or ``None`` on any failure (logged to + stderr). The caller treats ``None`` as a soft warning — the run still + proceeds; the matrix updater simply leaves the discharge-verdict tag off + that row. + """ + try: + import _lib_device_state as ds + except Exception as ex: + _log_stderr( + "before_run_record_device_state: _lib_device_state import failed: %s: %s" + % (type(ex).__name__, ex) + ) + return None + + serial = ds.get_device_serial(device) + target_dir = run_output_dir or _resolve_run_output_dir() + if not target_dir: + _log_stderr( + "before_run_record_device_state: cannot resolve per-run output dir on %s; " + "device_state.json NOT written (matrix row will lose its discharge tag)" % serial + ) + return None + + capability_record = ds.read_capability_cache(serial) + snapshot: Dict[str, Any] = ds.assemble_device_state_snapshot( + device, capability_record=capability_record + ) + + try: + os.makedirs(target_dir, exist_ok=True) + except Exception as ex: + _log_stderr( + "before_run_record_device_state: mkdir %s failed (continuing with write attempt): %s: %s" + % (target_dir, type(ex).__name__, ex) + ) + + target = op.join(target_dir, _FILENAME) + tmp = target + ".tmp" + try: + with open(tmp, "w", encoding="utf-8") as f: + json.dump(snapshot, f, indent=2, sort_keys=True, default=str) + f.write("\n") + os.replace(tmp, target) + except Exception as ex: + _log_stderr( + "before_run_record_device_state: write %s failed: %s: %s" + % (target, type(ex).__name__, ex) + ) + return None + + verdict = snapshot.get("discharge_verdict") or "unknown" + current_raw = snapshot.get("current_now_raw") + _log_stdout( + "before_run_record_device_state: wrote %s " + "(serial=%s discharge=%s current_now=%s)" + % (target, serial, verdict, current_raw) + ) + return target + + +def main(device, *args, **kwargs): + """Module entrypoint for direct AndroidRunner wiring (or chain-on-hook callers).""" + try: + record_device_state(device) + except Exception as ex: + _log_stderr( + "before_run_record_device_state.main: unexpected exception " + "(swallowed; run continues): %s: %s" + % (type(ex).__name__, ex) + ) diff --git a/examples/batterymanager/Scripts/detect_crash_anr.py b/examples/batterymanager/Scripts/detect_crash_anr.py new file mode 100644 index 000000000..f318d960c --- /dev/null +++ b/examples/batterymanager/Scripts/detect_crash_anr.py @@ -0,0 +1,363 @@ +"""Crash and ANR detection hook for AndroidRunner experiments. + +Classifies the app's exit state by parsing logcat output and inspecting +the device's foreground package, then writes ``crash_anr_status.json`` +next to the run outputs. The downstream tracking matrix consumes this +file to keep crash/ANR runs separate from clean energy aggregates +(see master-experiment.mdc constraint #4 - Crash/ANR isolation). + +Black-box principle: this module is purely passive - it reads logs and +queries dumpsys, never modifies the app, never instruments protected +APKs, and never re-enables debug visibility. + +Best-effort contract: every public function wraps I/O in try/except. +A failure here must NEVER crash the experiment. +""" + +from __future__ import annotations + +import argparse +import glob +import json +import os +import os.path as op +import re +import sys +import time + + +_FATAL_EXCEPTION_TOKEN = "FATAL EXCEPTION" +_TOMBSTONE_SIGNALS = ("SIGSEGV", "SIGABRT") +_TOMBSTONE_TOKEN = "tombstoned" + +_ANR_RE = re.compile(r"\bANR in ([A-Za-z][\w\.\$]*)") + +_SYSTEM_DIALOG_PATTERNS = ( + re.compile(r"Showing crash dialog"), + re.compile(r"Application Not Responding dialog"), + re.compile(r"\bam_anr\b"), +) + +_FOREGROUND_PKG_RE = re.compile(r"([A-Za-z][\w\.]*)/[\w\.\$]+") + + +def _line_is_tombstone_signal(line): + if _TOMBSTONE_TOKEN not in line: + return False + for sig in _TOMBSTONE_SIGNALS: + if sig in line: + return True + return False + + +def _context_window(lines, idx, before=1, after=2): + start = max(0, idx - before) + end = min(len(lines), idx + 1 + after) + return lines[start:idx], lines[idx + 1:end] + + +def classify(logcat_text, foreground_pkg, expected_pkg): + """Classify the app's exit state. + + Pure given inputs. Returns a dict with keys ``status``, ``evidence``, + ``expected_pkg``, ``foreground_pkg``. ``status`` is one of: + ``none``, ``crash``, ``anr``, ``system_error_dialog``, + ``lost_foreground``, ``unknown``. + + Detection priority (first matching rule wins for ``status``, but ALL + matching evidence across rules is collected): + + 1. ``crash`` - line contains ``FATAL EXCEPTION`` or + ``SIGSEGV``/``SIGABRT`` near ``tombstoned``. + 2. ``anr`` - line matches ``ANR in ``. + 3. ``system_error_dialog`` - ``Showing crash dialog``, + ``Application Not Responding dialog``, or + ``am_anr`` event. + 4. ``lost_foreground`` - expected_pkg is set, foreground_pkg differs, + and no crash/ANR/dialog signal was found. + 5. ``none`` - no signals AND foreground matches expected (or + expected is None). + 6. ``unknown`` - no signals AND expected_pkg is set but + foreground_pkg is missing. + """ + result = { + "status": "unknown", + "evidence": [], + "expected_pkg": expected_pkg, + "foreground_pkg": foreground_pkg, + } + + try: + text = logcat_text or "" + lines = text.splitlines() + + evidence = [] + crash_seen = False + anr_seen = False + sys_dialog_seen = False + + for i, line in enumerate(lines): + if _FATAL_EXCEPTION_TOKEN in line: + ctx_before, ctx_after = _context_window(lines, i, before=1, after=2) + evidence.append({ + "line": line, + "kind": "fatal_exception", + "context_before": ctx_before, + "context_after": ctx_after, + }) + crash_seen = True + + if _line_is_tombstone_signal(line): + ctx_before, ctx_after = _context_window(lines, i, before=1, after=2) + evidence.append({ + "line": line, + "kind": "tombstone_signal", + "context_before": ctx_before, + "context_after": ctx_after, + }) + crash_seen = True + + if _ANR_RE.search(line): + evidence.append({ + "line": line, + "kind": "anr", + }) + anr_seen = True + + for pat in _SYSTEM_DIALOG_PATTERNS: + if pat.search(line): + evidence.append({ + "line": line, + "kind": "system_dialog", + }) + sys_dialog_seen = True + break + + result["evidence"] = evidence + + if crash_seen: + result["status"] = "crash" + elif anr_seen: + result["status"] = "anr" + elif sys_dialog_seen: + result["status"] = "system_error_dialog" + elif expected_pkg and foreground_pkg and foreground_pkg != expected_pkg: + result["status"] = "lost_foreground" + elif expected_pkg and not foreground_pkg: + result["status"] = "unknown" + else: + result["status"] = "none" + except Exception as exc: + result["status"] = "unknown" + result["error"] = "classify_failed: {}: {}".format( + type(exc).__name__, exc + ) + + return result + + +def read_logcat_from_run_dir(run_output_dir): + """Find and return the run's logcat text. + + Prefers raw ``logcat*.txt`` (BatteryManager plugin's adb_log strategy + or any other text dump). Falls back to ``logcat*.csv`` so a CSV-only + run does not silently lose all classification context. Returns the + empty string when no candidate is found or any I/O step fails. + """ + try: + if not run_output_dir or not op.isdir(run_output_dir): + return "" + + text_candidates = [] + for pattern in ("logcat*.txt", "logcat.txt"): + text_candidates.extend(glob.glob( + op.join(run_output_dir, "**", pattern), recursive=True)) + text_candidates.extend(glob.glob( + op.join(run_output_dir, pattern))) + + csv_candidates = glob.glob( + op.join(run_output_dir, "**", "logcat*.csv"), recursive=True) + csv_candidates += glob.glob(op.join(run_output_dir, "logcat*.csv")) + + candidates = list(dict.fromkeys(text_candidates)) or list(dict.fromkeys(csv_candidates)) + if not candidates: + return "" + + candidates.sort( + key=lambda p: op.getmtime(p) if op.exists(p) else 0, + reverse=True, + ) + chosen = candidates[0] + with open(chosen, "r", encoding="utf-8", errors="replace") as f: + return f.read() + except Exception: + return "" + + +def get_foreground_pkg(device): + """Best-effort foreground package query via ``adb shell dumpsys``. + + Returns ``None`` on any failure (including missing ``device`` arg, + shell failures, or unparseable output). Never raises. + """ + try: + if device is None: + return None + + commands = ( + "dumpsys activity activities | grep mResumedActivity", + "dumpsys activity activities | grep mFocusedApp", + ) + + for cmd in commands: + try: + raw = device.shell(cmd) + except Exception: + continue + if raw is None: + continue + text = raw if isinstance(raw, str) else raw.decode("utf-8", errors="replace") + if not text or not text.strip(): + continue + match = _FOREGROUND_PKG_RE.search(text) + if match: + return match.group(1) + return None + except Exception: + return None + + +def write_status_json(run_output_dir, status): + """Write ``crash_anr_status.json`` into ``run_output_dir``. + + Adds a ``timestamp`` field if the input dict does not have one. + Returns the absolute path on success, or the empty string on failure. + """ + try: + if not run_output_dir: + return "" + try: + os.makedirs(run_output_dir, exist_ok=True) + except Exception: + pass + + out_path = op.join(run_output_dir, "crash_anr_status.json") + payload = dict(status) if isinstance(status, dict) else {"status": "unknown"} + payload.setdefault("timestamp", time.strftime("%Y-%m-%dT%H:%M:%S")) + + with open(out_path, "w", encoding="utf-8") as f: + json.dump(payload, f, indent=2, sort_keys=True) + return out_path + except Exception: + return "" + + +def cli(): + """Post-hoc CLI: read logcat from disk and write status JSON. + + Does NOT query a device (post-hoc by definition). Foreground package + is reported as ``None`` so classification can only resolve to + ``crash``/``anr``/``system_error_dialog``/``unknown``/``none`` based + on the logcat alone. + """ + parser = argparse.ArgumentParser( + description=( + "Classify crash/ANR status from a finished run's logcat " + "output and write crash_anr_status.json next to it." + ), + ) + parser.add_argument( + "--run-output-dir", + required=True, + help="Path to the run output directory containing the logcat file.", + ) + parser.add_argument( + "--expected-pkg", + default=None, + help="Expected foreground package (used only for status semantics).", + ) + args = parser.parse_args() + + try: + logcat_text = read_logcat_from_run_dir(args.run_output_dir) + result = classify( + logcat_text, + foreground_pkg=None, + expected_pkg=args.expected_pkg, + ) + out_path = write_status_json(args.run_output_dir, result) + sys.stdout.write( + "crash_anr_status: {} (evidence={}, wrote={})\n".format( + result.get("status"), + len(result.get("evidence", [])), + out_path or "", + ) + ) + except Exception as exc: + try: + sys.stderr.write("detect_crash_anr cli failed: {}\n".format(exc)) + except Exception: + pass + + +def main(device, *args, **kwargs): + """AndroidRunner-style hook. Best-effort - never raises. + + Resolves the per-run output dir from ``paths.OUTPUT_DIR``, queries + the device's foreground package, reads the run's logcat, classifies, + and writes ``crash_anr_status.json`` next to the run outputs. + """ + run_output_dir = "" + expected_pkg = None + foreground_pkg = None + logcat_text = "" + + try: + try: + import paths as ar_paths + run_output_dir = getattr(ar_paths, "OUTPUT_DIR", "") or "" + except Exception: + run_output_dir = "" + + try: + experiment = args[0] if args else None + if experiment is not None: + expected_pkg = getattr(experiment, "package", None) + if not expected_pkg: + expected_pkg = kwargs.get("app") or kwargs.get("package") + except Exception: + expected_pkg = None + + try: + foreground_pkg = get_foreground_pkg(device) + except Exception: + foreground_pkg = None + + try: + logcat_text = read_logcat_from_run_dir(run_output_dir) + except Exception: + logcat_text = "" + + result = classify(logcat_text, foreground_pkg, expected_pkg) + out_path = write_status_json(run_output_dir, result) + + try: + print( + "CRASH_ANR_DETECT: status={} foreground={} expected={} -> {}".format( + result.get("status"), + foreground_pkg, + expected_pkg, + out_path or "", + ) + ) + except Exception: + pass + except Exception as exc: + try: + sys.stderr.write("detect_crash_anr.main failed: {}\n".format(exc)) + except Exception: + pass + + +if __name__ == "__main__": + cli() diff --git a/examples/batterymanager/Scripts/interaction.py b/examples/batterymanager/Scripts/interaction.py index f4f5a9c36..304258422 100644 --- a/examples/batterymanager/Scripts/interaction.py +++ b/examples/batterymanager/Scripts/interaction.py @@ -1,5 +1,57 @@ # noinspection PyUnusedLocal def main(device, *args, **kwargs): - print('=INTERACTION=') - print((device.id)) - print((device.current_activity())) + import csv + import time + import os.path as op + + import paths + + print("=INTERACTION=") + print("DEVICE:", device.id) + + experiment = args[0] if len(args) >= 1 else None + package = getattr(experiment, "package", None) if experiment is not None else None + duration_s = getattr(experiment, "duration", 60) + + if not package: + package = kwargs.get("app") or kwargs.get("package") + + if not package: + print("INTERACTION: No package found; skipping.") + return + + # Start Monkey in background but silence stdout/stderr so AndroidRunner.Adb.shell + # doesn't treat Monkey output containing "error" as an adb error. + monkey_cmd = ( + "monkey -p {pkg} --throttle 100 -s 1234 -v 1000 " + "--ignore-crashes --ignore-timeouts --ignore-security-exceptions " + "--pct-flip 0 --pct-trackball 0" + ).format(pkg=package) + wrapped = "sh -c '{}'".format(monkey_cmd.replace("'", "'\"'\"'") + " >/dev/null 2>&1 &") + print("INTERACTION: starting Monkey for:", package) + device.shell(wrapped) + + # Energy sampling via sysfs/dumpsys (works on unrooted Android 15; companion app needs BATTERY_STATS). + # current_now: usually in µA (may be negative when discharging), voltage_now: usually in µV. + out_dir = getattr(paths, "OUTPUT_DIR", ".") + out_path = op.join(out_dir, "sysfs_power_{}_{}.csv".format(device.id, time.strftime("%Y.%m.%d_%H%M%S"))) + interval_s = 0.1 + end_t = time.time() + float(duration_s) + + def read_int(cmd): + try: + return int(device.shell(cmd).strip().splitlines()[-1]) + except Exception: + return None + + with open(out_path, "w", newline="") as f: + w = csv.writer(f) + w.writerow(["epoch_ms", "current_now_ua", "voltage_now_uv"]) + while time.time() < end_t: + now_ms = int(time.time() * 1000) + cur = read_int("cat /sys/class/power_supply/battery/current_now") + volt = read_int("cat /sys/class/power_supply/battery/voltage_now") + w.writerow([now_ms, cur, volt]) + time.sleep(interval_s) + + print("INTERACTION: wrote power samples to", out_path) diff --git a/examples/batterymanager/Scripts/interaction_appium.py b/examples/batterymanager/Scripts/interaction_appium.py new file mode 100644 index 000000000..311bdf333 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium.py @@ -0,0 +1,100 @@ +"""Generic Android Runner ``interaction`` hook for Appium black-box workloads. +Reads ``APPIUM_APP=`` (preferred) or ``APPIUM_WORKLOAD`` (legacy single-token +fallback) and dispatches to ``appium_android_tests..run_workload(experiment, +device)``. Failures write ``appium_status.json`` with an explicit ``failure_reason`` and +return instead of raising. See ``README-appium-hooks.md`` for env vars + troubleshooting. +Stdlib + ``appium_android_tests._lib.status`` only — no top-level ``import appium``. +""" +from __future__ import annotations + +import importlib +import json +import os +import os.path as op +import sys +import traceback + +_HERE = op.dirname(op.abspath(__file__)) +_FALLBACK_ROOT = "/home/irena/Documents/Master Experiment" +_RESERVED_WORKLOAD_NAMES = ("espresso_mirror", "generic") + +def _find_workspace_root(): + cur = _HERE + for _ in range(8): + if op.isfile(op.join(cur, "appium_android_tests", "__init__.py")): + return cur + parent = op.dirname(cur) + if parent == cur: + break + cur = parent + if op.isfile(op.join(_FALLBACK_ROOT, "appium_android_tests", "__init__.py")): + return _FALLBACK_ROOT + return None + +_WORKSPACE_ROOT = _find_workspace_root() +if _WORKSPACE_ROOT and _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +def _artifact_dir(): + try: + import paths as ar # type: ignore + od = getattr(ar, "OUTPUT_DIR", None) + if od: + return od + except Exception: + pass + o = os.environ.get("APPIUM_UI_DUMP_DIR", "").strip() + if o: + base = o.rstrip(os.sep) + return op.dirname(base) if base.endswith("appium_ui_dumps") else o + return os.getcwd() + +def _bail(reason): + try: + sys.stderr.write("INTERACTION_APPIUM: %s\n" % reason); sys.stderr.flush() + except Exception: + pass + payload = { + "session_created": False, "session_start_seconds": None, + "workload_started": False, "steps_executed": 0, + "session_connect_timeout_seconds": None, + "workload_mode": os.environ.get("APPIUM_WORKLOAD", "").strip(), + "failure_reason": reason, "session_timeout": False, + } + ad = _artifact_dir() + try: + from appium_android_tests._lib.status import write_status_raw + write_status_raw(ad, payload); return + except Exception: + pass + try: + os.makedirs(ad, exist_ok=True) + with open(op.join(ad, "appium_status.json"), "w", encoding="utf-8") as f: + json.dump(payload, f, indent=2, sort_keys=True); f.write("\n") + except Exception: + pass + +def main(device, *args, **kwargs): + experiment = args[0] if len(args) >= 1 else None + if _WORKSPACE_ROOT is None: + return _bail("appium_android_tests package not on sys.path (workspace-root resolution failed from %s)" % _HERE) + app_id = os.environ.get("APPIUM_APP", "").strip() + if not app_id: + legacy = os.environ.get("APPIUM_WORKLOAD", "").strip() + if not legacy: + return _bail("APPIUM_APP env var missing (set APPIUM_APP= in the experiment JSON)") + if "_" in legacy or legacy in _RESERVED_WORKLOAD_NAMES: + return _bail("APPIUM_APP env var ambiguous: '%s' (looks like a workload-mode token, not an app id)" % legacy) + app_id = legacy + try: + module = importlib.import_module("appium_android_tests." + app_id) + except ModuleNotFoundError: + return _bail("per-app module not found: appium_android_tests.%s" % app_id) + run_workload = getattr(module, "run_workload", None) + if run_workload is None: + return _bail("per-app module has no run_workload: appium_android_tests.%s" % app_id) + try: + run_workload(experiment=experiment, device=device) + except Exception as ex: + traceback.print_exc() + return _bail("per-app run_workload raised %s: %s" % (type(ex).__name__, ex)) diff --git a/examples/batterymanager/Scripts/interaction_appium_TEMPLATE.py b/examples/batterymanager/Scripts/interaction_appium_TEMPLATE.py new file mode 100644 index 000000000..e276d46e8 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_TEMPLATE.py @@ -0,0 +1,20 @@ +"""Per-app Appium hook template. Copy to ``Scripts/interaction_appium_.py``, +replace ```` everywhere (folder under ``appium_android_tests/``), and reference +from the experiment JSON as +``"interaction": "examples/batterymanager/Scripts/interaction_appium_.py"``. +See ``README-appium-hooks.md`` for the env-var contract. +""" +from __future__ import annotations +import os, os.path as op, sys + +_HERE = op.dirname(op.abspath(__file__)) +_WORKSPACE_ROOT = op.abspath(op.join(_HERE, *([os.pardir] * 4))) +for _p in (_WORKSPACE_ROOT, _HERE): + if _p not in sys.path: + sys.path.insert(0, _p) + + +def main(device, *args, **kwargs): + os.environ["APPIUM_APP"] = "" + import interaction_appium + interaction_appium.main(device, *args, **kwargs) diff --git a/examples/batterymanager/Scripts/interaction_appium_androidskk.py b/examples/batterymanager/Scripts/interaction_appium_androidskk.py new file mode 100644 index 000000000..392f72970 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_androidskk.py @@ -0,0 +1,12 @@ +"""Thin AndroidRunner hook for AndroidSKK.""" +from __future__ import annotations +import os, sys +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WS = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WS not in sys.path: sys.path.insert(0, _WS) +os.environ.setdefault("APPIUM_APP", "androidskk") + +def main(device, *args, **kwargs): + from appium_android_tests import androidskk + experiment = args[0] if len(args) >= 1 else None + androidskk.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_another_notes.py b/examples/batterymanager/Scripts/interaction_appium_another_notes.py new file mode 100644 index 000000000..81f31b1a3 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_another_notes.py @@ -0,0 +1,21 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for maltaisn another-notes.""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +os.environ.setdefault("APPIUM_APP", "another_notes") + + +def main(device, *args, **kwargs): + from appium_android_tests import another_notes + experiment = args[0] if len(args) >= 1 else None + another_notes.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_avnc.py b/examples/batterymanager/Scripts/interaction_appium_avnc.py new file mode 100644 index 000000000..e3cf497d4 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_avnc.py @@ -0,0 +1,31 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for gujjwal00 AVNC. + +Actual Appium / UiAutomator2 logic lives in +``appium_android_tests.avnc`` (per-app scenarios + selectors) on top of the shared library +``appium_android_tests._lib`` (driver lifecycle, scoring, coverage, timing, status). + +Set ``"interaction_covers_duration": true`` in the experiment JSON so ``NativeExperiment`` does +not double-sleep this run. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +# Tag this run as the avnc app for tracking-matrix purposes. +os.environ.setdefault("APPIUM_APP", "avnc") + + +def main(device, *args, **kwargs): + from appium_android_tests import avnc + + experiment = args[0] if len(args) >= 1 else None + avnc.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_calculator.py b/examples/batterymanager/Scripts/interaction_appium_calculator.py new file mode 100644 index 000000000..aa169cf82 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_calculator.py @@ -0,0 +1,31 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for exponentialGroth Calculator. + +Actual Appium / UiAutomator2 logic lives in :mod:`appium_android_tests.calculator` +(per-app scenarios + selectors) on top of the shared library +:mod:`appium_android_tests._lib`. + +Set ``"interaction_covers_duration": true`` in the experiment JSON so ``NativeExperiment`` +does not double-sleep this run. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +# Tag this run as the calculator app for tracking-matrix purposes. +os.environ.setdefault("APPIUM_APP", "calculator") + + +def main(device, *args, **kwargs): + from appium_android_tests import calculator + + experiment = args[0] if len(args) >= 1 else None + calculator.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_crcontainer.py b/examples/batterymanager/Scripts/interaction_appium_crcontainer.py new file mode 100644 index 000000000..50bcd0ffa --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_crcontainer.py @@ -0,0 +1,24 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for curiouslearning CRcontainer. + +Actual Appium logic lives in ``appium_android_tests.crcontainer``. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +os.environ.setdefault("APPIUM_APP", "crcontainer") + + +def main(device, *args, **kwargs): + from appium_android_tests import crcontainer + experiment = args[0] if len(args) >= 1 else None + crcontainer.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_diaguard.py b/examples/batterymanager/Scripts/interaction_appium_diaguard.py new file mode 100644 index 000000000..8480eaeec --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_diaguard.py @@ -0,0 +1,31 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for Faltenreich Diaguard. + +Actual Appium / UiAutomator2 logic lives in +``appium_android_tests.diaguard`` (per-app scenarios + selectors) on top of the shared library +``appium_android_tests._lib`` (driver lifecycle, scoring, coverage, timing, status). + +Set ``"interaction_covers_duration": true`` in the experiment JSON so ``NativeExperiment`` does +not double-sleep this run. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +# Tag this run as the diaguard app for tracking-matrix purposes. +os.environ.setdefault("APPIUM_APP", "diaguard") + + +def main(device, *args, **kwargs): + from appium_android_tests import diaguard + + experiment = args[0] if len(args) >= 1 else None + diaguard.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_documenter.py b/examples/batterymanager/Scripts/interaction_appium_documenter.py new file mode 100644 index 000000000..92aff6552 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_documenter.py @@ -0,0 +1,39 @@ +# noinspection PyUnusedLocal +""" +Thin Android Runner ``interaction`` hook for ViliusSutkus89 Documenter. + +The actual Appium / UiAutomator2 logic lives in the workspace package +``appium_android_tests.documenter`` (per-app scenarios + selectors) on top of the shared library +``appium_android_tests._lib`` (driver lifecycle, scoring, coverage, timing, status). See +``appium_android_tests/CONVENTIONS.md`` for the folder layout and the contract expected of each +per-app module. + +Workload selection, env-var contract, and produced artifacts (``appium_workload_coverage.jsonl``, +``espresso_mirror_scenario_report.{json,txt}``, ``appium_status.json``) are documented in +``appium_android_tests/documenter/scenarios.py``. Set ``"interaction_covers_duration": true`` in +the experiment JSON so ``NativeExperiment`` does not double-sleep this run. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +# Tag this run as the documenter app so after_experiment.py's tracking-matrix +# updater records `app=documenter` (not the legacy default `metronome`). Uses +# setdefault so an explicit caller-set APPIUM_APP still wins. Belt-and- +# suspenders against the after_experiment.py fallback chain (post-2026-05-12 fix). +os.environ.setdefault("APPIUM_APP", "documenter") + + +def main(device, *args, **kwargs): + from appium_android_tests import documenter + + experiment = args[0] if len(args) >= 1 else None + documenter.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_gallerywall.py b/examples/batterymanager/Scripts/interaction_appium_gallerywall.py new file mode 100644 index 000000000..45acbe3b9 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_gallerywall.py @@ -0,0 +1,24 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for bossly GalleryWall. + +Actual Appium logic lives in :mod:`appium_android_tests.gallerywall`. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +os.environ.setdefault("APPIUM_APP", "gallerywall") + + +def main(device, *args, **kwargs): + from appium_android_tests import gallerywall + experiment = args[0] if len(args) >= 1 else None + gallerywall.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_horoscapp.py b/examples/batterymanager/Scripts/interaction_appium_horoscapp.py new file mode 100644 index 000000000..2996f2d4e --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_horoscapp.py @@ -0,0 +1,59 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for angelsoft071 HoroscApp. + +Before delegating to the per-app workload, this hook **grants +``android.permission.CAMERA``** via ``pm grant`` so the palmistry-fragment CameraX preview +opens cleanly (no system permission dialog interrupts the workload). The app is guaranteed +to be installed at this point — AndroidRunner installs the APK between ``before_run`` and +``after_launch``, and ``interaction`` runs after that. + +Actual Appium / UiAutomator2 logic lives in ``appium_android_tests.horoscapp``. + +Set ``"interaction_covers_duration": true`` in the experiment JSON so ``NativeExperiment`` +does not double-sleep this run. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +PACKAGE = "com.angelsoft.horoscapp" + +# Tag this run as the horoscapp app for tracking-matrix purposes. +os.environ.setdefault("APPIUM_APP", "horoscapp") + + +def _grant_camera(device) -> None: + """Pre-grant CAMERA permission. Idempotent — pm grant is a no-op if already granted.""" + try: + out = device.shell("pm grant %s android.permission.CAMERA" % PACKAGE) or "" + try: + sys.stdout.write( + "interaction_appium_horoscapp: granted CAMERA permission to %s " + "(stdout=%r)\n" % (PACKAGE, out[:160]) + ) + except Exception: + pass + except Exception as exc: + try: + sys.stderr.write( + "interaction_appium_horoscapp: CAMERA grant failed (continuing — palmistry " + "scenario may see a permission dialog): %s: %s\n" % (type(exc).__name__, exc) + ) + except Exception: + pass + + +def main(device, *args, **kwargs): + _grant_camera(device) + from appium_android_tests import horoscapp + + experiment = args[0] if len(args) >= 1 else None + horoscapp.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_iamspeed.py b/examples/batterymanager/Scripts/interaction_appium_iamspeed.py new file mode 100644 index 000000000..0131f77a5 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_iamspeed.py @@ -0,0 +1,82 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for ViliusSutkus89 IamSpeed. + +Pre-grants location + notification runtime permissions so the workload starts in a known +state without system permission dialogs interrupting scenarios. + +**Critical:** GPS must NOT be enabled before launch. On Android 14+ the app's +``SpeedListenerService`` requires the ``FOREGROUND_SERVICE_LOCATION`` permission to start +a location-typed foreground service — but ``IamSpeed v1.2.5``'s manifest doesn't declare +it. When permission + location + GPS are all on, ``IamSpeedFragment.serviceCanBeStartedOnStartup`` +auto-launches the service which then crashes with +``SecurityException: Starting FGS with type location ... requires permissions +FOREGROUND_SERVICE_LOCATION`` (kills the app process, system shows "Close" +crash dialog). By keeping GPS off at launch, the fragment shows ``button_enable_gps`` +and the service auto-start never fires. Scenarios still exercise the bytecode-heavy +paths (Navigation Component fragment transitions, action-bar menu items, AppCompat +preference inflation, system-settings round-trip via ``button_enable_location``). + +Actual Appium logic lives in :mod:`appium_android_tests.iamspeed`. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +PACKAGE = "com.viliussutkus89.iamspeed.debug" + +# Tag this run as the iamspeed app for tracking-matrix purposes. +os.environ.setdefault("APPIUM_APP", "iamspeed") + + +def _grant_runtime_perms(device) -> None: + """Pre-grant location + notification permissions. Idempotent.""" + perms = [ + "android.permission.ACCESS_FINE_LOCATION", + "android.permission.ACCESS_COARSE_LOCATION", + "android.permission.POST_NOTIFICATIONS", + ] + for p in perms: + try: + out = device.shell("pm grant %s %s" % (PACKAGE, p)) or "" + try: + sys.stdout.write( + "interaction_appium_iamspeed: granted %s to %s (stdout=%r)\n" + % (p, PACKAGE, out[:120]) + ) + except Exception: + pass + except Exception as exc: + try: + sys.stderr.write( + "interaction_appium_iamspeed: grant %s failed (continuing): " + "%s: %s\n" % (p, type(exc).__name__, exc) + ) + except Exception: + pass + + +def _disable_gps(device) -> None: + """Force GPS off so IamSpeed's auto-start of the location-typed foreground service never + fires (the v1.2.5 manifest is missing FOREGROUND_SERVICE_LOCATION → fatal SecurityException + on Android 14+). Non-fatal on failure; the scenarios still pass action-only if GPS is on. + """ + try: + device.shell("settings put secure location_mode 0") # 0 = OFF + except Exception: + pass + + +def main(device, *args, **kwargs): + _grant_runtime_perms(device) + _disable_gps(device) + from appium_android_tests import iamspeed + experiment = args[0] if len(args) >= 1 else None + iamspeed.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_itsok.py b/examples/batterymanager/Scripts/interaction_appium_itsok.py new file mode 100644 index 000000000..76908f6f3 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_itsok.py @@ -0,0 +1,23 @@ +"""Thin AndroidRunner ``interaction`` hook for qubacy ItsOK. Per-app logic lives in +:mod:`appium_android_tests.itsok`. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +os.environ.setdefault("APPIUM_APP", "itsok") + + +def main(device, *args, **kwargs): + from appium_android_tests import itsok + + experiment = args[0] if len(args) >= 1 else None + itsok.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_keep_recipe.py b/examples/batterymanager/Scripts/interaction_appium_keep_recipe.py new file mode 100644 index 000000000..c105250bf --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_keep_recipe.py @@ -0,0 +1,12 @@ +"""Thin AndroidRunner hook for Keep-Recipe.""" +from __future__ import annotations +import os, sys +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WS = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WS not in sys.path: sys.path.insert(0, _WS) +os.environ.setdefault("APPIUM_APP", "keep_recipe") + +def main(device, *args, **kwargs): + from appium_android_tests import keep_recipe + experiment = args[0] if len(args) >= 1 else None + keep_recipe.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_keepitup.py b/examples/batterymanager/Scripts/interaction_appium_keepitup.py new file mode 100644 index 000000000..3957df038 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_keepitup.py @@ -0,0 +1,12 @@ +"""Thin AndroidRunner hook for keepitup.""" +from __future__ import annotations +import os, sys +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WS = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WS not in sys.path: sys.path.insert(0, _WS) +os.environ.setdefault("APPIUM_APP", "keepitup") + +def main(device, *args, **kwargs): + from appium_android_tests import keepitup + experiment = args[0] if len(args) >= 1 else None + keepitup.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_linkhub.py b/examples/batterymanager/Scripts/interaction_appium_linkhub.py new file mode 100644 index 000000000..af47f5851 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_linkhub.py @@ -0,0 +1,41 @@ +# noinspection PyUnusedLocal +""" +Thin Android Runner ``interaction`` hook for AmrDeveloper LinkHub. + +The actual Appium / UiAutomator2 logic lives in the workspace package +``appium_android_tests.linkhub`` (per-app scenarios + selectors + scoring) on top +of the shared library ``appium_android_tests._lib`` (driver lifecycle, scoring +primitives, coverage, timing, status). See ``appium_android_tests/CONVENTIONS.md`` +for the folder layout and the contract expected of each per-app module. + +Workload model, env-var contract, and produced artifacts +(``appium_workload_coverage.jsonl``, ``linkhub_scenario_report.{json,txt}``, +``appium_status.json``) are documented in +``appium_android_tests/linkhub/scenarios.py``. Set +``"interaction_covers_duration": true`` in the experiment JSON so +``NativeExperiment`` does not double-sleep this run. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +# Tag this run as the linkhub app so after_experiment.py's tracking-matrix +# updater records `app=linkhub` (not the legacy default `metronome`). Uses +# setdefault so an explicit caller-set APPIUM_APP still wins. Belt-and- +# suspenders against the after_experiment.py fallback chain (post-2026-05-12 fix). +os.environ.setdefault("APPIUM_APP", "linkhub") + + +def main(device, *args, **kwargs): + from appium_android_tests import linkhub + + experiment = args[0] if len(args) >= 1 else None + linkhub.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_medtimer.py b/examples/batterymanager/Scripts/interaction_appium_medtimer.py new file mode 100644 index 000000000..ecc95af10 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_medtimer.py @@ -0,0 +1,12 @@ +"""Thin AndroidRunner hook for MedTimer.""" +from __future__ import annotations +import os, sys +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WS = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WS not in sys.path: sys.path.insert(0, _WS) +os.environ.setdefault("APPIUM_APP", "medtimer") + +def main(device, *args, **kwargs): + from appium_android_tests import medtimer + experiment = args[0] if len(args) >= 1 else None + medtimer.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_metronome.py b/examples/batterymanager/Scripts/interaction_appium_metronome.py new file mode 100644 index 000000000..6b0e111cb --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_metronome.py @@ -0,0 +1,40 @@ +# noinspection PyUnusedLocal +""" +Thin Android Runner ``interaction`` hook for Kr0oked Metronome. + +The actual Appium / UiAutomator2 logic lives in the workspace package +``appium_android_tests.metronome`` (per-app scenarios + selectors) on top of the shared library +``appium_android_tests._lib`` (driver lifecycle, scoring, coverage, timing, status). See +``appium_android_tests/CONVENTIONS.md`` for the folder layout and the contract expected of each +per-app module. + +Workload selection, env-var contract, and produced artifacts (``appium_workload_coverage.jsonl``, +``espresso_mirror_scenario_report.{json,txt}``, ``appium_status.json``) are documented in +``appium_android_tests/metronome/scenarios.py``. Set ``"interaction_covers_duration": true`` in the +experiment JSON so ``NativeExperiment`` does not double-sleep this run. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +# Tag this run as the metronome app so after_experiment.py's tracking-matrix +# updater records `app=metronome` reliably (instead of relying on it being +# the legacy default — which was the source of the 2026-05-12 misclassification +# bug for non-Metronome runs). Uses setdefault so an explicit caller-set +# APPIUM_APP still wins. +os.environ.setdefault("APPIUM_APP", "metronome") + + +def main(device, *args, **kwargs): + from appium_android_tests import metronome + + experiment = args[0] if len(args) >= 1 else None + metronome.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_metronome_espresso_mirror.py b/examples/batterymanager/Scripts/interaction_appium_metronome_espresso_mirror.py new file mode 100644 index 000000000..77261a79a --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_metronome_espresso_mirror.py @@ -0,0 +1,35 @@ +# noinspection PyUnusedLocal +""" +Espresso-aligned Appium hook — same Android Runner lifecycle as +``interaction_appium_metronome.py``, but forces ``APPIUM_WORKLOAD=espresso_mirror`` so the shared +implementation in ``appium_android_tests.metronome`` runs the InstrumentedTest-inspired scenario +suite (``initialState``, edit/slider coupling, ``tempoMarkings`` walk, error touches). + +Use this script in experiment JSON when you want to **classify** runs explicitly as +"Espresso-surface" vs the default baseline / exploratory ``interaction_appium_metronome.py`` hook. +Equivalent: keep the default hook and ``export APPIUM_WORKLOAD=espresso_mirror``. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +# Tag this run as the metronome app so after_experiment.py's tracking-matrix +# updater records `app=metronome` reliably (independent of the legacy default). +# Uses setdefault so an explicit caller-set APPIUM_APP still wins. +os.environ.setdefault("APPIUM_APP", "metronome") + + +def main(device, *args, **kwargs): + os.environ["APPIUM_WORKLOAD"] = "espresso_mirror" + from appium_android_tests import metronome + + experiment = args[0] if len(args) >= 1 else None + metronome.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_pdfviewer.py b/examples/batterymanager/Scripts/interaction_appium_pdfviewer.py new file mode 100644 index 000000000..5cb09a25d --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_pdfviewer.py @@ -0,0 +1,31 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for afreakyelf Pdf-Viewer sample app. + +Actual Appium / UiAutomator2 logic lives in +``appium_android_tests.pdfviewer`` (per-app scenarios + selectors) on top of the shared +library ``appium_android_tests._lib`` (driver lifecycle, scoring, coverage, timing, status). + +Set ``"interaction_covers_duration": true`` in the experiment JSON so ``NativeExperiment`` +does not double-sleep this run. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +# Tag this run as the pdfviewer app for tracking-matrix purposes. +os.environ.setdefault("APPIUM_APP", "pdfviewer") + + +def main(device, *args, **kwargs): + from appium_android_tests import pdfviewer + + experiment = args[0] if len(args) >= 1 else None + pdfviewer.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_podaura.py b/examples/batterymanager/Scripts/interaction_appium_podaura.py new file mode 100644 index 000000000..686b44d2d --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_podaura.py @@ -0,0 +1,25 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for SkyD666 PodAura. + +Actual Appium / UiAutomator2 logic lives in ``appium_android_tests.podaura``. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +os.environ.setdefault("APPIUM_APP", "podaura") + + +def main(device, *args, **kwargs): + from appium_android_tests import podaura + + experiment = args[0] if len(args) >= 1 else None + podaura.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_poetskingdom.py b/examples/batterymanager/Scripts/interaction_appium_poetskingdom.py new file mode 100644 index 000000000..b00fe13d8 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_poetskingdom.py @@ -0,0 +1,12 @@ +"""Thin AndroidRunner interaction hook for PoetsKingdom.""" +from __future__ import annotations +import os, sys +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: sys.path.insert(0, _WORKSPACE_ROOT) +os.environ.setdefault("APPIUM_APP", "poetskingdom") + +def main(device, *args, **kwargs): + from appium_android_tests import poetskingdom + experiment = args[0] if len(args) >= 1 else None + poetskingdom.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_repertoire.py b/examples/batterymanager/Scripts/interaction_appium_repertoire.py new file mode 100644 index 000000000..b9ef440ed --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_repertoire.py @@ -0,0 +1,41 @@ +# noinspection PyUnusedLocal +""" +Thin Android Runner ``interaction`` hook for klalumiere Repertoire. + +The actual Appium / UiAutomator2 logic lives in the workspace package +``appium_android_tests.repertoire`` (per-app scenarios + selectors + scoring) on +top of the shared library ``appium_android_tests._lib``. See +``appium_android_tests/CONVENTIONS.md`` for the folder layout and the contract +expected of each per-app module. + +Env-var contract, file-fixture requirement, and produced artifacts +(``appium_workload_coverage.jsonl``, ``espresso_mirror_scenario_report.{json,txt}``, +``appium_status.json``) are documented in +``appium_android_tests/repertoire/scenarios.py``. Set +``"interaction_covers_duration": true`` in the experiment JSON so +``NativeExperiment`` does not double-sleep this run. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +# Tag this run as the repertoire app so after_experiment.py's tracking-matrix +# updater records `app=repertoire` (not the legacy default `metronome`). Uses +# setdefault so an explicit caller-set APPIUM_APP still wins. Belt-and- +# suspenders against the after_experiment.py fallback chain (post-2026-05-12 fix). +os.environ.setdefault("APPIUM_APP", "repertoire") + + +def main(device, *args, **kwargs): + from appium_android_tests import repertoire + + experiment = args[0] if len(args) >= 1 else None + repertoire.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_tipuous.py b/examples/batterymanager/Scripts/interaction_appium_tipuous.py new file mode 100644 index 000000000..bac6f4a14 --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_tipuous.py @@ -0,0 +1,40 @@ +# noinspection PyUnusedLocal +""" +Thin Android Runner ``interaction`` hook for JoshLudahl Tipuous. + +The actual Appium / UiAutomator2 logic lives in the workspace package +``appium_android_tests.tipuous`` (per-app scenarios + selectors) on top of the shared library +``appium_android_tests._lib`` (driver lifecycle, scoring, coverage, timing, status). See +``appium_android_tests/CONVENTIONS.md`` for the folder layout and the contract expected of each +per-app module. + +Workload selection, env-var contract, and produced artifacts (``appium_workload_coverage.jsonl``, +``espresso_mirror_scenario_report.{json,txt}``, ``appium_status.json``) are documented in +``appium_android_tests/tipuous/scenarios.py``. Set ``"interaction_covers_duration": true`` in the +experiment JSON so ``NativeExperiment`` does not double-sleep this run. +""" + +from __future__ import annotations + +import os +import sys + + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +# Tag this run as the tipuous app so after_experiment.py's tracking-matrix +# updater records `app=tipuous` (not the legacy default `metronome`). Uses +# setdefault so an explicit caller-set APPIUM_APP still wins. Belt-and- +# suspenders against the after_experiment.py fallback chain (which also +# infers the app from the APK-basename slug — see post-2026-05-12 fix). +os.environ.setdefault("APPIUM_APP", "tipuous") + + +def main(device, *args, **kwargs): + from appium_android_tests import tipuous + + experiment = args[0] if len(args) >= 1 else None + tipuous.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_appium_ytalarm.py b/examples/batterymanager/Scripts/interaction_appium_ytalarm.py new file mode 100644 index 000000000..288e2e1ab --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_appium_ytalarm.py @@ -0,0 +1,47 @@ +# noinspection PyUnusedLocal +"""Thin AndroidRunner ``interaction`` hook for turtton YtAlarm.""" +from __future__ import annotations +import os, sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_WORKSPACE_ROOT = os.path.abspath(os.path.join(_HERE, *([os.pardir] * 4))) +if _WORKSPACE_ROOT not in sys.path: + sys.path.insert(0, _WORKSPACE_ROOT) + +os.environ.setdefault("APPIUM_APP", "ytalarm") + + +def main(device, *args, **kwargs): + from appium_android_tests import ytalarm + # Pre-grant POST_NOTIFICATIONS to YtAlarm BEFORE the workload session + # connects. AndroidRunner's monkey-launch step (which precedes this hook) + # may inflate the runtime permission dialog that blocks the alarm-list UI + # underneath. We grant via ``pm grant``, then force-stop + restart the app + # so the (now-granted) permission state takes effect and the dialog + # disappears on re-launch. + try: + import subprocess + subprocess.run( + ["adb", "-s", device.id, "shell", "pm", "grant", + "net.turtton.ytalarm", "android.permission.POST_NOTIFICATIONS"], + check=False, timeout=4, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # Force-stop to clear any blocking dialog (the perm prompt remains + # rendered until the requesting Activity is destroyed). + subprocess.run( + ["adb", "-s", device.id, "shell", "am", "force-stop", + "net.turtton.ytalarm"], + check=False, timeout=4, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # Re-launch MainActivity cleanly. + subprocess.run( + ["adb", "-s", device.id, "shell", "am", "start", "-W", "-n", + "net.turtton.ytalarm/net.turtton.ytalarm.activity.MainActivity"], + check=False, timeout=8, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + import time as _t + _t.sleep(1.5) + except Exception: + pass + experiment = args[0] if len(args) >= 1 else None + ytalarm.run_workload(experiment=experiment, device=device) diff --git a/examples/batterymanager/Scripts/interaction_monkey_only.py b/examples/batterymanager/Scripts/interaction_monkey_only.py new file mode 100644 index 000000000..d25ed69bb --- /dev/null +++ b/examples/batterymanager/Scripts/interaction_monkey_only.py @@ -0,0 +1,31 @@ +# noinspection PyUnusedLocal +def main(device, *args, **kwargs): + import time + + # Same Monkey workload as interaction.py, but no sysfs loop. + # Run length is the NativeExperiment `duration` (ms in JSON → seconds in code): after + # this returns, NativeExperiment still calls time.sleep(self.duration), so the profiled + # window matches a normal native run (Monkey started, then one sleep in the parent). + + print("=INTERACTION_MONKEY_ONLY=") + print("DEVICE:", device.id) + + experiment = args[0] if len(args) >= 1 else None + package = getattr(experiment, "package", None) if experiment is not None else None + if not package: + package = kwargs.get("app") or kwargs.get("package") + + if not package: + print("INTERACTION_MONKEY_ONLY: No package found; skipping.") + return + + monkey_cmd = ( + "monkey -p {pkg} --throttle 100 -s 1234 -v 1000 " + "--ignore-crashes --ignore-timeouts --ignore-security-exceptions " + "--pct-flip 0 --pct-trackball 0" + ).format(pkg=package) + wrapped = "sh -c '{}'".format(monkey_cmd.replace("'", "'\"'\"'") + " >/dev/null 2>&1 &") + print("INTERACTION_MONKEY_ONLY: starting Monkey (background) for:", package) + device.shell(wrapped) + print("INTERACTION_MONKEY_ONLY: no sysfs; power data comes from BatteryManager profiler (if enabled).") + return diff --git a/examples/batterymanager/Scripts/update_tracking_matrix.py b/examples/batterymanager/Scripts/update_tracking_matrix.py new file mode 100755 index 000000000..458b5d7b7 --- /dev/null +++ b/examples/batterymanager/Scripts/update_tracking_matrix.py @@ -0,0 +1,1341 @@ +#!/usr/bin/env python3 +"""Append (or replace) one row in ``specs/tracking_matrix.csv`` for an Android Runner run. + +Stdlib-only. Best-effort: this script never raises out to the caller. On any +internal error it writes a row with ``notes="update_failed: "`` and exits 0 +so the experiment lifecycle is never disturbed. + +Usage:: + + python3 update_tracking_matrix.py \ + --app metronome --variant baseline --device pixel3 \ + --run-output-dir /path/to/android-runner/examples/batterymanager/output/2026.05.05_235701 + +The matrix file is located by walking up from this script until a directory +named ``specs/`` is found that contains ``tracking_matrix.csv`` (override with +``--matrix-path``). See ``specs/TRACKING_MATRIX.md`` for the schema. +""" + +from __future__ import annotations + +import argparse +import csv +import datetime as _dt +import glob +import hashlib +import io +import json +import os +import os.path as op +import sys +import traceback +from typing import Iterable, Optional + + +COLUMNS = [ + "app", + "variant", + "device", + "run_id", + "timestamp", + "installed", + "appium_session", + "workload_started", + "scenario_action_pass", + "scenario_strict_pass", + "energy_source", + "aggregated_energy_mwh", + "crash_anr_status", + "apk_path", + "apk_sha256", + "apk_storage", + # E0.T7: Three-window energy split (post-hoc analysis of the same single + # measurement; the runtime profiler boundaries are unchanged). + "energy_pre_workload_mwh", + "energy_workload_only_mwh", + "energy_whole_window_mwh", + "duration_pre_workload_s", + "duration_workload_s", + "duration_whole_window_s", + # E1.5.T5: Aux samplers (CPU + memory). Empty when the run pre-dates + # E1.5.T1/T2 or when the sampler init failed — never raises. + "cpu_avg_pct", + "cpu_p95_pct", + "mem_pss_avg_mb", + "mem_pss_max_mb", + "notes", +] + +# Columns added by E0.T7 (kept here so the back-fill / migration scripts can +# enumerate them without re-listing every name). +ENERGY_WINDOW_COLUMNS = ( + "energy_pre_workload_mwh", + "energy_workload_only_mwh", + "energy_whole_window_mwh", + "duration_pre_workload_s", + "duration_workload_s", + "duration_whole_window_s", +) + +# Columns added by E1.5.T5. Same convention as ENERGY_WINDOW_COLUMNS. +AUX_SAMPLER_COLUMNS = ( + "cpu_avg_pct", + "cpu_p95_pct", + "mem_pss_avg_mb", + "mem_pss_max_mb", +) + +VALID_VARIANTS = ("baseline", "r8", "allatori", "bangcle") +VALID_DEVICES = ("pixel3", "pixel6", "pixel9") +VALID_CRASH_STATUSES = ( + "none", + "crash", + "anr", + "system_error_dialog", + "lost_foreground", + "unknown", +) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _bool_str(value: Optional[bool]) -> str: + if value is True: + return "true" + if value is False: + return "false" + return "unknown" + + +def _now_iso_utc() -> str: + return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def _read_json(path: str) -> Optional[dict]: + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except (OSError, ValueError): + return None + + +def _walk_up_for_matrix(start: str) -> Optional[str]: + """Walk up from ``start`` looking for ``specs/tracking_matrix.csv``.""" + cur = op.abspath(start) + last = None + while cur and cur != last: + candidate = op.join(cur, "specs", "tracking_matrix.csv") + if op.isfile(candidate): + return candidate + last, cur = cur, op.dirname(cur) + return None + + +def _resolve_matrix_path(override: Optional[str]) -> str: + """Locate ``specs/tracking_matrix.csv`` (script location → workspace root).""" + if override: + return op.abspath(override) + here = op.dirname(op.abspath(__file__)) + found = _walk_up_for_matrix(here) + if found: + return found + cwd = _walk_up_for_matrix(os.getcwd()) + if cwd: + return cwd + # Fallback: assume default repo layout (script in + # android-runner/examples/batterymanager/Scripts → workspace 4 levels up). + repo_root = op.abspath(op.join(here, op.pardir, op.pardir, op.pardir, op.pardir)) + return op.join(repo_root, "specs", "tracking_matrix.csv") + + +# --------------------------------------------------------------------------- +# Artifact discovery +# --------------------------------------------------------------------------- + + +def _find_first(paths: Iterable[str]) -> Optional[str]: + for p in paths: + if p and op.isfile(p): + return p + return None + + +def _find_appium_status(run_output_dir: str) -> Optional[str]: + """``appium_status.json`` lives under ``data///`` after a run. + + Tolerate it being directly under the run dir (older layouts / smoke runs). + """ + direct = op.join(run_output_dir, "appium_status.json") + if op.isfile(direct): + return direct + matches = sorted(glob.glob(op.join(run_output_dir, "data", "*", "*", "appium_status.json"))) + if matches: + return matches[0] + matches = sorted(glob.glob(op.join(run_output_dir, "data", "**", "appium_status.json"), recursive=True)) + return matches[0] if matches else None + + +def _find_scenario_report(run_output_dir: str) -> Optional[str]: + direct = op.join(run_output_dir, "espresso_mirror_scenario_report.json") + if op.isfile(direct): + return direct + matches = sorted(glob.glob(op.join(run_output_dir, "data", "*", "*", "espresso_mirror_scenario_report.json"))) + if matches: + return matches[0] + matches = sorted(glob.glob( + op.join(run_output_dir, "data", "**", "espresso_mirror_scenario_report.json"), + recursive=True, + )) + return matches[0] if matches else None + + +def _find_crash_anr_status(run_output_dir: str) -> Optional[str]: + """``crash_anr_status.json`` is produced by the (future) E0.T5 hook.""" + return _find_first([ + op.join(run_output_dir, "crash_anr_status.json"), + *sorted(glob.glob(op.join(run_output_dir, "data", "*", "*", "crash_anr_status.json"))), + *sorted(glob.glob(op.join(run_output_dir, "data", "**", "crash_anr_status.json"), recursive=True)), + ]) + + +def _find_aux_summary(run_output_dir: str) -> Optional[str]: + """Locate ``aux/aux_summary.json`` written by E1.5.T5's `aux_postprocess.py`. + + The post-processor reads Android Runner's built-in `android` profiler CSV + (under ``paths.OUTPUT_DIR/android/``) and writes its aggregates to + ``paths.OUTPUT_DIR/aux/aux_summary.json``. From the top-level run dir + that's ``data///aux/aux_summary.json``. We also accept a + top-level ``aux/aux_summary.json`` for back-fill / smoke tests. + """ + filename = "aux_summary.json" + return _find_first([ + op.join(run_output_dir, "aux", filename), + *sorted(glob.glob(op.join(run_output_dir, "data", "*", "*", "aux", filename))), + *sorted(glob.glob( + op.join(run_output_dir, "data", "**", "aux", filename), + recursive=True, + )), + ]) + + +def _read_aux_summary(run_output_dir: str) -> Optional[dict]: + """Read ``aux/aux_summary.json``. Returns ``None`` when missing OR malformed. + + Best-effort: pre-aux runs and runs where the `android` profiler block was + not enabled simply return ``None`` here, the caller leaves the cells empty, + and ``notes`` gets no annotation (absent aux summary is a valid state). + """ + path = _find_aux_summary(run_output_dir) + if not path: + return None + return _read_json(path) + + +def _aux_stat(payload: Optional[dict], column: str, stat_key: str) -> Optional[float]: + """Pull ``payload["columns"][column][stat_key]`` if all three exist and are numeric. + + The new schema (post 2026-05-08T19:30Z) is:: + + { + "csv_path": "...", + "samples": N, + "columns": { + "cpu": {"avg":..., "p50":..., "p95":..., "max":..., "min":..., "n":...}, + "mem": {"avg":..., ...} + } + } + """ + if not isinstance(payload, dict): + return None + columns = payload.get("columns") + if not isinstance(columns, dict): + return None + block = columns.get(column) + if not isinstance(block, dict): + return None + val = block.get(stat_key) + if val is None: + return None + try: + return float(val) + except (TypeError, ValueError): + return None + + +def _find_apk_meta(run_output_dir: str) -> Optional[str]: + """``apk_meta.json`` is dropped by the experiment template / pre_run hook. + + Schema (all keys optional):: + + { + "apk_path": "app_repositories_newest/_built_apks/metronome/bangcle.apk", + "apk_sha256": "", // optional; will be (re)computed if file is readable + "apk_storage": "local" // or "gh-release:/" + // or "zenodo:/" + } + """ + return _find_first([ + op.join(run_output_dir, "apk_meta.json"), + *sorted(glob.glob(op.join(run_output_dir, "data", "*", "*", "apk_meta.json"))), + *sorted(glob.glob(op.join(run_output_dir, "data", "**", "apk_meta.json"), recursive=True)), + ]) + + +def _find_device_state(run_output_dir: str) -> Optional[str]: + """Locate ``device_state.json`` written by E0.T8's per-run hook. + + ``before_run_record_device_state.record_device_state`` writes it into + ``paths.OUTPUT_DIR``, which from the top-level run dir is + ``data///device_state.json``. We also accept a + top-level ``device_state.json`` for back-fill / smoke tests. + + Schema (all 9 fields from the E0.T8 spec, see ``_lib_device_state.assemble_device_state_snapshot``). + Pre-E0.T8 runs simply lack the file → ``None`` → caller leaves the + matrix row's discharge tag empty. + """ + return _find_first([ + op.join(run_output_dir, "device_state.json"), + *sorted(glob.glob(op.join(run_output_dir, "data", "*", "*", "device_state.json"))), + *sorted(glob.glob( + op.join(run_output_dir, "data", "**", "device_state.json"), + recursive=True, + )), + ]) + + +def _read_device_state(run_output_dir: str) -> Optional[dict]: + """Read ``device_state.json``. Returns ``None`` when missing OR malformed.""" + path = _find_device_state(run_output_dir) + if not path: + return None + return _read_json(path) + + +def _device_state_notes(payload: Optional[dict]) -> list[str]: + """Convert ``device_state.json`` → list of ``notes`` strings for the matrix row. + + The contract is: only emit annotations when something is *abnormal*. A + fully-clean run (verified discharge, brightness locked at 128/manual, + BATTERY_STATS granted) produces an empty list — the ``notes`` column + stays uncluttered. + + Annotations (worst-case takes precedence in the operator's eye): + + - ``energy_invalid_usb_supplying`` — ``discharge_verdict == + "suspected_supplying"`` (charge IC dominating; energy reading + cannot be compared cross-variant). + - ``energy_validity_unknown`` — ``discharge_verdict == "unknown"`` + (current_now unreadable; treat as USB-supplied for safety). + - ``companion_apk_legacy_no_battery_stats_declared`` — ``battery_stats_granted + == False`` AFTER the 2026-05-08 evening rebuild. **Retracted + semantics:** this used to be ``battery_stats_not_granted`` and was + believed to flag bad readings. The 2026-05-08 evening A/B test on + Pixel 3 + the AOSP API surface (``current.txt``) both prove the + grant does NOT change ``BATTERY_PROPERTY_CURRENT_NOW``. The tag + now means: *the upstream S2-group companion APK is installed + instead of the rebuilt fork from + ``_external/batterymanager-companion-fork/``*. That's a regression + to detect — install the rebuilt APK and re-grant — but the + readings themselves are NOT invalidated by this annotation alone. + See `docs/MEASUREMENT_NOISE_SOURCES.md` § 1 for the full + retraction. + - ``brightness_not_locked`` — adaptive mode still on or value drift + > ±32 from the locked default of 128. + """ + if not isinstance(payload, dict): + return [] + notes: list[str] = [] + verdict = payload.get("discharge_verdict") + current_raw = payload.get("current_now_raw") + if verdict == "suspected_supplying": + notes.append("energy_invalid_usb_supplying=current_now_raw=%s" % current_raw) + elif verdict == "unknown": + notes.append("energy_validity_unknown=current_now_unreadable") + granted = payload.get("battery_stats_granted") + if granted is False: + notes.append("companion_apk_legacy_no_battery_stats_declared") + brightness = payload.get("screen_brightness") or {} + if isinstance(brightness, dict): + mode = brightness.get("mode") + value = brightness.get("value") + if mode == 1: + notes.append("brightness_adaptive_not_locked") + if value is not None: + try: + if abs(int(value) - 128) > 32: + notes.append("brightness_drift=%s" % value) + except (TypeError, ValueError): + pass + return notes + + +def _charge_dominant_notes(run_output_dir: str) -> list[str]: + """Detect § 1b float-charge masking from the raw BatteryManager CSV. + + The signature of float-charge masking (see + `docs/MEASUREMENT_NOISE_SOURCES.md` § 1b) is that the charge controller + is in active-charge mode during the workload, so the per-sample + ``BATTERY_PROPERTY_CURRENT_NOW`` reads positive (charging) more than + half the time. The energy integrator multiplies ``|I| × V``, so when + the workload's discharge contribution is dominated by charger ramp, + the resulting ``Energy (J)`` cannot be compared cross-variant. + + Threshold: > 50 % positive samples ⇒ tag the row with + ``energy_invalid_charge_dominant=%_positive_samples`` so the + cross-variant aggregator can drop the row from comparisons until + Epic 1.6 hub-ctrl lands. The < 50 % case stays untagged (clean + discharge dominates). + + Best-effort: never raises, returns ``[]`` on any I/O / parse failure + (the absence of the tag means "we couldn't tell", not "clean"). + """ + try: + candidates = sorted(glob.glob(op.join( + run_output_dir, "data", "*", "*", "batterymanager", "logcat_*.csv", + ))) + if not candidates: + candidates = sorted(glob.glob(op.join( + run_output_dir, "data", "**", "batterymanager", "logcat_*.csv", + ), recursive=True)) + if not candidates: + return [] + # Pick the largest CSV (most samples) if there are multiple — same + # tie-break the energy-window slicer uses. + path = max(candidates, key=lambda p: op.getsize(p)) + cur_keys = ( + "BATTERY_PROPERTY_CURRENT_NOW", "current_now_ua", + "current_uA", "current_ua", + ) + n = 0 + n_pos = 0 + with open(path, "r", encoding="utf-8", newline="") as f: + reader = csv.DictReader(f) + for row in reader: + raw = _first_present(row, cur_keys) + if raw is None: + continue + try: + v = float(raw) + except (TypeError, ValueError): + continue + n += 1 + if v > 0: + n_pos += 1 + if n < 30: + # Too few samples to draw a verdict — typical for smoke runs. + return [] + pct = 100.0 * n_pos / n + if pct > 50.0: + return ["energy_invalid_charge_dominant=%.0f%%_positive_samples" % pct] + return [] + except Exception: # noqa: BLE001 - never raise + return [] + + +# --------------------------------------------------------------------------- +# APK identity (sha256 + provenance string) +# --------------------------------------------------------------------------- + + +def _resolve_workspace_root(matrix_path: str) -> str: + """Workspace root is the directory that contains ``specs/tracking_matrix.csv``.""" + return op.dirname(op.dirname(op.abspath(matrix_path))) + + +def _sha256_file(path: str) -> Optional[str]: + """Streaming SHA-256 of ``path``. Returns ``None`` if the file is unreadable.""" + if not path or not op.isfile(path): + return None + h = hashlib.sha256() + try: + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + except OSError: + return None + return h.hexdigest() + + +def _normalize_apk_path(apk_path: Optional[str], workspace_root: str) -> tuple[str, Optional[str]]: + """Return ``(stored_apk_path, abs_apk_path_or_None)``. + + ``stored_apk_path`` is what we write into the CSV: the path relative to + the workspace root if the APK lives inside the workspace, otherwise the + absolute path. Empty string if no APK was supplied. + """ + if not apk_path: + return "", None + abs_path = op.abspath(apk_path) + if op.isfile(abs_path): + try: + common = op.commonpath([abs_path, workspace_root]) + except ValueError: + common = "" + stored = op.relpath(abs_path, workspace_root) if common == workspace_root else abs_path + return stored, abs_path + # File not found yet — still record the requested path for trace; no hash. + return apk_path, None + + +def _resolve_apk_identity( + *, + cli_apk_path: Optional[str], + cli_apk_sha256: Optional[str], + cli_apk_storage: Optional[str], + apk_meta: Optional[dict], + workspace_root: str, +) -> tuple[str, str, str, list[str]]: + """Combine CLI args + ``apk_meta.json`` into ``(apk_path, apk_sha256, apk_storage, notes)``. + + Precedence (highest first): CLI flag > ``apk_meta.json`` > empty. + SHA-256 is recomputed from the file whenever it is readable, even if a + value was provided, so a mismatch surfaces as a ``notes`` warning rather + than a silent lie. + """ + notes: list[str] = [] + meta = apk_meta if isinstance(apk_meta, dict) else {} + + apk_path_raw = cli_apk_path or meta.get("apk_path") or "" + apk_storage = cli_apk_storage or meta.get("apk_storage") or ("local" if apk_path_raw else "") + declared_sha = cli_apk_sha256 or meta.get("apk_sha256") or "" + + apk_path_stored, abs_path = _normalize_apk_path(apk_path_raw, workspace_root) + + computed_sha = _sha256_file(abs_path) if abs_path else None + if computed_sha: + if declared_sha and declared_sha.lower() != computed_sha.lower(): + notes.append( + "apk_sha256_mismatch=declared:%s,computed:%s" + % (declared_sha[:12], computed_sha[:12]) + ) + apk_sha256 = computed_sha + else: + if apk_path_raw and not abs_path: + notes.append("apk_file_missing=%s" % apk_path_raw) + apk_sha256 = declared_sha # may still be empty + + return apk_path_stored, apk_sha256, apk_storage, notes + + +# --------------------------------------------------------------------------- +# Energy aggregation +# --------------------------------------------------------------------------- + + +def _joules_to_mwh(joules: float) -> float: + """1 mWh = 3.6 J → mWh = J / 3.6.""" + return float(joules) / 3.6 + + +def _read_batterymanager_aggregate(run_output_dir: str) -> Optional[float]: + """Return aggregated energy in mWh from ``Aggregated_Results_Batterymanager.csv``.""" + path = op.join(run_output_dir, "Aggregated_Results_Batterymanager.csv") + if not op.isfile(path): + return None + try: + with open(path, "r", encoding="utf-8") as f: + reader = csv.DictReader(f) + joules: list[float] = [] + for row in reader: + # Prefer the trapezoidal integral; fall back to the simple sum. + value = row.get("Energy trapz (J)") or row.get("Energy simple (J)") + if value is None or value == "": + continue + try: + joules.append(float(value)) + except ValueError: + continue + if not joules: + return None + # AndroidRunner aggregates per-(device, subject, run); the experiment we + # care about typically yields one row, but if there are several + # repetitions, sum them so the matrix entry covers the whole run dir. + return _joules_to_mwh(sum(joules)) + except OSError: + return None + + +def _read_sysfs_energy_summary(run_output_dir: str) -> Optional[float]: + """Return aggregated energy in mWh from a pre-computed ``sysfs_energy_summary.csv``.""" + candidates = sorted(glob.glob(op.join(run_output_dir, "**", "sysfs_energy_summary.csv"), recursive=True)) + if not candidates: + return None + try: + with open(candidates[0], "r", encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + for key in ("energy_mwh", "Energy mWh", "energy_total_mwh"): + if row.get(key): + try: + return float(row[key]) + except ValueError: + continue + for key in ("energy_j", "Energy (J)", "Energy J", "energy_trapz_j"): + if row.get(key): + try: + return _joules_to_mwh(float(row[key])) + except ValueError: + continue + except OSError: + return None + return None + + +def _integrate_sysfs_power(run_output_dir: str) -> Optional[float]: + """Trapezoidal integral of ``sysfs_power_*.csv`` samples → mWh. + + Schema (see ``Scripts/interaction.py``):: + + epoch_ms,current_now_ua,voltage_now_uv + + ``current_now`` may be negative (discharging convention); we take the + magnitude because the matrix records *energy*, not signed flow. + """ + candidates = sorted(glob.glob(op.join(run_output_dir, "**", "sysfs_power_*.csv"), recursive=True)) + if not candidates: + return None + total_joules = 0.0 + any_samples = False + for path in candidates: + try: + with open(path, "r", encoding="utf-8") as f: + reader = csv.DictReader(f) + prev_t: Optional[float] = None + prev_p: Optional[float] = None + for row in reader: + try: + t = int(row["epoch_ms"]) / 1000.0 + cur_ua = float(row["current_now_ua"]) + volt_uv = float(row["voltage_now_uv"]) + except (KeyError, ValueError, TypeError): + continue + power_w = abs(cur_ua * 1e-6) * (volt_uv * 1e-6) + if prev_t is not None and prev_p is not None: + dt = t - prev_t + if 0 < dt < 60: # skip pathological gaps + total_joules += 0.5 * (power_w + prev_p) * dt + any_samples = True + prev_t, prev_p = t, power_w + except OSError: + continue + if not any_samples or total_joules <= 0: + return None + return _joules_to_mwh(total_joules) + + +def _aggregate_energy(run_output_dir: str) -> tuple[str, Optional[float]]: + """Return ``(energy_source, aggregated_energy_mwh)``.""" + bm = _read_batterymanager_aggregate(run_output_dir) + if bm is not None: + return "batterymanager", bm + sysfs_summary = _read_sysfs_energy_summary(run_output_dir) + if sysfs_summary is not None: + return "sysfs", sysfs_summary + sysfs_integ = _integrate_sysfs_power(run_output_dir) + if sysfs_integ is not None: + return "sysfs", sysfs_integ + return "none", None + + +# --------------------------------------------------------------------------- +# E0.T7 - Three-window energy split (post-hoc analysis) +# --------------------------------------------------------------------------- +# +# The Android Runner BatteryManager plugin records *one* per-sample CSV per +# run (``data///batterymanager/logcat__*.csv``). The +# whole-run energy is already covered by the legacy ``aggregated_energy_mwh`` +# column. E0.T7 slices that **same** stream into three windows and integrates +# each one independently, so a thesis query can separate (1) protection +# startup cost, (2) steady-state UI cost, (3) total user-facing cost from a +# single measurement. No extra wall-clock runtime, no extra device wear, and +# the runtime profiler boundaries are deliberately *not* moved (reverting +# this section restores the pre-T7 behaviour). +# +# Window definitions: +# pre_workload : profiler_start -> first appium scenario ts +# workload_only: first appium scenario ts -> last appium scenario ts +# whole_window : profiler_start -> profiler_stop (== legacy aggregated_energy_mwh) + + +def _parse_log_timestamp_to_host_s(line: str) -> Optional[float]: + """Parse the leading ``YYYY-MM-DD HH:MM:SS,mmm`` from a Python-logging line. + + The log timestamp is naive local time of the host that ran the experiment. + Convert to UTC epoch seconds via the system tz so it can be compared to + the (already-UTC) Appium scenario ``ts`` values and to BatteryManager + sample timestamps after offset alignment. + """ + if not line or len(line) < 23: + return None + head = line[:23] # "2026-05-08 10:57:04,403" + try: + naive = _dt.datetime.strptime(head, "%Y-%m-%d %H:%M:%S,%f") + except ValueError: + return None + try: + local_aware = naive.astimezone() # interprets naive as local time (Py 3.6+) + return local_aware.timestamp() + except (ValueError, OSError): + return None + + +def _parse_iso_utc_to_s(value: object) -> Optional[float]: + """Parse an ISO-8601 UTC string (``...Z`` or ``+00:00``) into epoch seconds.""" + if not isinstance(value, str): + return None + text = value.strip() + if not text: + return None + if text.endswith("Z"): + text = text[:-1] + "+00:00" + try: + d = _dt.datetime.fromisoformat(text) + except ValueError: + return None + if d.tzinfo is None: + d = d.replace(tzinfo=_dt.timezone.utc) + try: + return d.timestamp() + except (ValueError, OSError): + return None + + +def _parse_profiler_boundaries(run_output_dir: str) -> tuple[Optional[float], Optional[float]]: + """Return ``(start_host_s, stop_host_s)`` from ``experiment.log``. + + AndroidRunner emits multiple ``Profilers:Stop profiling`` lines (the real + end-of-measurement plus a later one during subject aggregation). We take + the **first** Start and the **first Stop after that Start** so the window + matches the actual battery-sampling phase. + """ + log_path = op.join(run_output_dir, "experiment.log") + if not op.isfile(log_path): + return None, None + start_host_s: Optional[float] = None + stop_host_s: Optional[float] = None + try: + with open(log_path, "r", encoding="utf-8", errors="replace") as f: + for line in f: + if start_host_s is None: + if "Profilers:Start profiling" in line or "Profilers: Start profiling" in line: + start_host_s = _parse_log_timestamp_to_host_s(line) + else: + if "Profilers:Stop profiling" in line or "Profilers: Stop profiling" in line: + stop_host_s = _parse_log_timestamp_to_host_s(line) + if stop_host_s is not None: + break + except OSError: + return None, None + return start_host_s, stop_host_s + + +def _parse_appium_workload_boundaries(run_output_dir: str) -> tuple[Optional[float], Optional[float]]: + """Return ``(first_ts_s, last_ts_s)`` from ``appium_workload_coverage.jsonl``.""" + candidates = sorted(glob.glob(op.join( + run_output_dir, "data", "*", "*", "appium_workload_coverage.jsonl", + ))) + if not candidates: + candidates = sorted(glob.glob(op.join( + run_output_dir, "data", "**", "appium_workload_coverage.jsonl", + ), recursive=True)) + if not candidates: + return None, None + path = candidates[0] + try: + with open(path, "r", encoding="utf-8") as f: + lines = [ln.strip() for ln in f if ln.strip()] + except OSError: + return None, None + if not lines: + return None, None + try: + first = json.loads(lines[0]) + except ValueError: + first = {} + try: + last = json.loads(lines[-1]) + except ValueError: + last = {} + return _parse_iso_utc_to_s(first.get("ts")), _parse_iso_utc_to_s(last.get("ts")) + + +def _voltage_raw_to_volts(volt_raw: float) -> float: + """Robustly convert a BatteryManager voltage reading to volts. + + The BatteryManager Java API ``EXTRA_VOLTAGE`` is documented in **mV** + (typical Li-ion values 3000-4400). The sysfs ``voltage_now`` node is in + **uV** (3000000-4400000). A unit-less ``volts`` reading would be < 10. + Ranges below are widened to cover degraded cells and weird devices. + """ + if volt_raw <= 0: + return 0.0 + if volt_raw < 100: + return float(volt_raw) # already volts + if volt_raw < 100_000: + return float(volt_raw) / 1000.0 # mV -> V + return float(volt_raw) / 1_000_000.0 # uV -> V + + +def _parse_batterymanager_logcat(csv_path: str) -> list[tuple[float, float, float]]: + """Parse a raw per-sample CSV into ``[(device_ts_ms, current_uA, voltage_V), ...]``. + + Tolerates schema variations across BatteryManager plugin versions. Returns + an empty list on any unrecoverable error. + """ + ts_keys = ("Timestamp", "timestamp", "epoch_ms", "ts") + cur_keys = ("BATTERY_PROPERTY_CURRENT_NOW", "current_now_ua", "current_uA", "current_ua") + volt_keys = ("EXTRA_VOLTAGE", "voltage_now_uv", "voltage_uV", "voltage_uv", "voltage") + samples: list[tuple[float, float, float]] = [] + try: + with open(csv_path, "r", encoding="utf-8", newline="") as f: + reader = csv.DictReader(f) + for row in reader: + ts_raw = _first_present(row, ts_keys) + cur_raw = _first_present(row, cur_keys) + volt_raw = _first_present(row, volt_keys) + if ts_raw is None or cur_raw is None or volt_raw is None: + continue + try: + ts_v = float(ts_raw) + cur_v = float(cur_raw) + volt_v_raw = float(volt_raw) + except (TypeError, ValueError): + continue + # Normalise timestamp to ms (10 digits = seconds, 13 = ms). + if ts_v < 1e12: + ts_v *= 1000.0 + samples.append((ts_v, cur_v, _voltage_raw_to_volts(volt_v_raw))) + except OSError: + return [] + samples.sort(key=lambda t: t[0]) + return samples + + +def _first_present(row: dict, keys: Iterable[str]): + for k in keys: + if k in row and row[k] not in ("", None): + return row[k] + return None + + +def _trapezoidal_energy_mwh( + host_samples: list[tuple[float, float]], + t_a: Optional[float], + t_b: Optional[float], +) -> Optional[float]: + """Trapezoidal integration of ``(host_s, power_W)`` samples over ``[t_a, t_b]``. + + Includes only samples that fall inside the window; boundary fractions are + truncated. With ~10 Hz BatteryManager sampling and >=60 s windows that is + sub-1% loss — well within the +/-10% sanity bound the matrix uses. + """ + if t_a is None or t_b is None or not (t_b > t_a): + return None + in_window = [(t, p) for t, p in host_samples if t_a <= t <= t_b] + if len(in_window) < 2: + return None + total_joules = 0.0 + prev_t, prev_p = in_window[0] + for t, p in in_window[1:]: + dt = t - prev_t + if 0 < dt < 60: # skip pathological gaps (matches sysfs integrator) + total_joules += 0.5 * (p + prev_p) * dt + prev_t, prev_p = t, p + if total_joules <= 0: + return None + return _joules_to_mwh(total_joules) + + +def _empty_energy_windows(*notes: str) -> dict: + return { + "energy_pre_workload_mwh": None, + "energy_workload_only_mwh": None, + "energy_whole_window_mwh": None, + "duration_pre_workload_s": None, + "duration_workload_s": None, + "duration_whole_window_s": None, + "notes_addendum": list(notes), + } + + +def _compute_energy_windows(run_output_dir: str) -> dict: + """Compute pre_workload / workload_only / whole_window energy + duration. + + Best-effort: never raises. On any failure the six numeric values are + ``None`` and ``notes_addendum`` carries an ``energy_window_split_unresolved=`` + annotation that the caller appends to the row's ``notes`` column. + """ + try: + notes: list[str] = [] + + # 1) Locate the raw per-sample CSV. + csv_candidates = sorted(glob.glob(op.join( + run_output_dir, "data", "*", "*", "batterymanager", "logcat_*.csv", + ))) + if not csv_candidates: + csv_candidates = sorted(glob.glob(op.join( + run_output_dir, "data", "**", "batterymanager", "logcat_*.csv", + ), recursive=True)) + if not csv_candidates: + return _empty_energy_windows("energy_window_split_unresolved=raw_csv_missing") + if len(csv_candidates) > 1: + # Take the largest file (most samples) and document the choice. + csv_path = max(csv_candidates, key=lambda p: op.getsize(p)) + notes.append("multi_logcat_csvs_picked_largest") + else: + csv_path = csv_candidates[0] + + # 2) Parse raw samples. + samples = _parse_batterymanager_logcat(csv_path) + if len(samples) < 2: + notes.append("energy_window_split_unresolved=raw_csv_unparseable_or_empty") + return _empty_energy_windows(*notes) + first_sample_device_ms = samples[0][0] + last_sample_device_ms = samples[-1][0] + + # 3) Parse the profiler boundaries from experiment.log; fall back to + # sample boundaries if the log is unavailable / unparseable. We + # treat the host clock and the device clock as potentially offset + # (BatteryManager timestamps are device-local epoch ms; on Pixels + # that are still on factory time the offset can be years). + profiler_start_host_s, profiler_stop_host_s = _parse_profiler_boundaries(run_output_dir) + if profiler_start_host_s is None: + profiler_start_host_s = first_sample_device_ms / 1000.0 + notes.append("profiler_start_fallback_to_first_sample") + if profiler_stop_host_s is None: + profiler_stop_host_s = last_sample_device_ms / 1000.0 + notes.append("profiler_stop_fallback_to_last_sample") + + # 4) Compute the device-clock -> host-clock offset using the start + # anchor (first sample is recorded ~immediately after Start + # profiling, so this anchor is tight to within one sample period). + device_to_host_offset_s = profiler_start_host_s - (first_sample_device_ms / 1000.0) + host_samples: list[tuple[float, float]] = [] + for ts_ms, cur_ua, volt_v in samples: + host_t = ts_ms / 1000.0 + device_to_host_offset_s + power_w = abs(cur_ua * 1e-6) * volt_v # |I| (A) * V (V) = W + host_samples.append((host_t, power_w)) + + # 5) Whole window. + energy_whole = _trapezoidal_energy_mwh( + host_samples, profiler_start_host_s, profiler_stop_host_s, + ) + duration_whole = ( + profiler_stop_host_s - profiler_start_host_s + if energy_whole is not None else None + ) + + # 6) Workload sub-window + pre-workload remainder. + workload_start_host_s, workload_end_host_s = _parse_appium_workload_boundaries(run_output_dir) + if workload_start_host_s is None or workload_end_host_s is None: + notes.append("appium_workload_boundaries_missing") + return { + "energy_pre_workload_mwh": None, + "energy_workload_only_mwh": None, + "energy_whole_window_mwh": energy_whole, + "duration_pre_workload_s": None, + "duration_workload_s": None, + "duration_whole_window_s": duration_whole, + "notes_addendum": notes, + } + + # Clamp workload boundaries inside the whole window so the slices stay + # non-overlapping if the JSONL ts drifts past Stop profiling. + if workload_start_host_s < profiler_start_host_s: + workload_start_host_s = profiler_start_host_s + notes.append("workload_start_clamped_to_profiler_start") + if workload_end_host_s > profiler_stop_host_s: + workload_end_host_s = profiler_stop_host_s + notes.append("workload_end_clamped_to_profiler_stop") + + energy_workload = _trapezoidal_energy_mwh( + host_samples, workload_start_host_s, workload_end_host_s, + ) + duration_workload = ( + workload_end_host_s - workload_start_host_s + if energy_workload is not None else None + ) + energy_pre = _trapezoidal_energy_mwh( + host_samples, profiler_start_host_s, workload_start_host_s, + ) + duration_pre = ( + workload_start_host_s - profiler_start_host_s + if energy_pre is not None else None + ) + + return { + "energy_pre_workload_mwh": energy_pre, + "energy_workload_only_mwh": energy_workload, + "energy_whole_window_mwh": energy_whole, + "duration_pre_workload_s": duration_pre, + "duration_workload_s": duration_workload, + "duration_whole_window_s": duration_whole, + "notes_addendum": notes, + } + except Exception as ex: # noqa: BLE001 - best-effort, never raise + return _empty_energy_windows( + "energy_window_split_unresolved=%s:%s" % (type(ex).__name__, ex) + ) + + +# --------------------------------------------------------------------------- +# Row composition +# --------------------------------------------------------------------------- + + +def _scenario_pass(report: Optional[dict]) -> tuple[Optional[bool], Optional[bool]]: + """Return ``(action_pass, strict_pass)`` from the espresso-mirror report.""" + if not isinstance(report, dict): + return None, None + summary = report.get("summary") or {} + try: + action = int(summary.get("total_action_only_pass", 0)) > 0 + except (TypeError, ValueError): + action = None + try: + strict = int(summary.get("total_strict_ui_pass", 0)) > 0 + except (TypeError, ValueError): + strict = None + return action, strict + + +def _appium_flags(status: Optional[dict]) -> tuple[Optional[bool], Optional[bool], Optional[bool]]: + """Return ``(installed, appium_session, workload_started)``. + + ``installed`` is inferred — Appium can only create a session for an + installed package, so ``session_created=true`` ⇒ installed. Otherwise we + cannot tell from this artifact alone, so leave it ``unknown``. + """ + if not isinstance(status, dict): + return None, None, None + session = status.get("session_created") + workload = status.get("workload_started") + session_b = bool(session) if isinstance(session, bool) else None + workload_b = bool(workload) if isinstance(workload, bool) else None + installed_b: Optional[bool] = True if session_b else None + return installed_b, session_b, workload_b + + +def _crash_status(payload: Optional[dict]) -> str: + if not isinstance(payload, dict): + return "unknown" + raw = payload.get("status") or payload.get("crash_anr_status") + if isinstance(raw, str) and raw in VALID_CRASH_STATUSES: + return raw + return "unknown" + + +def _compose_row( + *, + app: str, + variant: str, + device: str, + run_id: str, + run_output_dir: str, + workspace_root: str, + cli_apk_path: Optional[str], + cli_apk_sha256: Optional[str], + cli_apk_storage: Optional[str], +) -> dict: + appium_status = _read_json(_find_appium_status(run_output_dir) or "") + scenario_report = _read_json(_find_scenario_report(run_output_dir) or "") + crash_payload = _read_json(_find_crash_anr_status(run_output_dir) or "") + apk_meta = _read_json(_find_apk_meta(run_output_dir) or "") + # E1.5.T5 — aux summary written by `aux_postprocess.py` from the built-in + # `android` profiler's CSV. May be absent on pre-aux runs or when the + # `android` profiler block is not enabled in the experiment config; in + # both cases the cells stay empty and `notes` is unaffected. + aux_summary = _read_aux_summary(run_output_dir) + # E0.T8 — per-run device-state snapshot. Pre-E0.T8 runs simply lack the + # file → device_notes is empty and the notes column gets no annotation. + device_state = _read_device_state(run_output_dir) + + installed, session, workload = _appium_flags(appium_status) + action_pass, strict_pass = _scenario_pass(scenario_report) + + # E0.T7 - compute the three windows from the raw per-sample CSV. Always + # safe to call: never raises, returns Nones + notes_addendum on failure. + windows = _compute_energy_windows(run_output_dir) + if windows.get("energy_whole_window_mwh") is not None: + # New code path: the per-sample integrator owns the whole-window + # number. Source is implicitly batterymanager (raw CSV). + energy_source = "batterymanager" + energy_mwh: Optional[float] = windows["energy_whole_window_mwh"] + else: + # Fall back to the legacy aggregator (Aggregated_Results_Batterymanager.csv + # or the sysfs paths) so the column never silently regresses. + energy_source, energy_mwh = _aggregate_energy(run_output_dir) + apk_path, apk_sha256, apk_storage, apk_notes = _resolve_apk_identity( + cli_apk_path=cli_apk_path, + cli_apk_sha256=cli_apk_sha256, + cli_apk_storage=cli_apk_storage, + apk_meta=apk_meta, + workspace_root=workspace_root, + ) + + notes_parts: list[str] = [] + if appium_status is None: + notes_parts.append("appium_status_missing") + if scenario_report is None: + notes_parts.append("scenario_report_missing") + if crash_payload is None: + notes_parts.append("crash_anr_status_missing") + if not apk_path and not apk_meta: + notes_parts.append("apk_meta_missing") + failure_reason = (appium_status or {}).get("failure_reason") if isinstance(appium_status, dict) else None + if failure_reason: + notes_parts.append("appium_failure_reason=%s" % failure_reason) + notes_parts.extend(apk_notes) + notes_parts.extend(windows.get("notes_addendum") or []) + notes_parts.extend(_device_state_notes(device_state)) + notes_parts.extend(_charge_dominant_notes(run_output_dir)) + + return { + "app": app, + "variant": variant, + "device": device, + "run_id": run_id, + "timestamp": _now_iso_utc(), + "installed": _bool_str(installed), + "appium_session": _bool_str(session), + "workload_started": _bool_str(workload), + "scenario_action_pass": _bool_str(action_pass), + "scenario_strict_pass": _bool_str(strict_pass), + "energy_source": energy_source, + "aggregated_energy_mwh": "" if energy_mwh is None else ("%.6f" % energy_mwh), + "crash_anr_status": _crash_status(crash_payload), + "apk_path": apk_path, + "apk_sha256": apk_sha256, + "apk_storage": apk_storage, + "energy_pre_workload_mwh": _fmt_window(windows.get("energy_pre_workload_mwh")), + "energy_workload_only_mwh": _fmt_window(windows.get("energy_workload_only_mwh")), + "energy_whole_window_mwh": _fmt_window(windows.get("energy_whole_window_mwh")), + "duration_pre_workload_s": _fmt_window(windows.get("duration_pre_workload_s"), precision=3), + "duration_workload_s": _fmt_window(windows.get("duration_workload_s"), precision=3), + "duration_whole_window_s": _fmt_window(windows.get("duration_whole_window_s"), precision=3), + # E1.5.T5 — aux columns sourced from the built-in `android` profiler's + # CSV via `aux_postprocess.py`. Built-in column semantics: + # - "cpu" = system-wide CPU% (TOTAL line of `dumpsys cpuinfo`) + # - "mem" = subject-app PSS in KB (TOTAL row of `dumpsys meminfo `) + # The built-in does NOT split app-vs-system CPU or break PSS into + # java/native heap; if those are needed later, extend `aux_postprocess`. + "cpu_avg_pct": _fmt_window(_aux_stat(aux_summary, "cpu", "avg"), precision=2), + "cpu_p95_pct": _fmt_window(_aux_stat(aux_summary, "cpu", "p95"), precision=2), + "mem_pss_avg_mb": _fmt_window(_kb_to_mb(_aux_stat(aux_summary, "mem", "avg")), precision=2), + "mem_pss_max_mb": _fmt_window(_kb_to_mb(_aux_stat(aux_summary, "mem", "max")), precision=2), + "notes": "; ".join(notes_parts), + } + + +def _kb_to_mb(value: Optional[float]) -> Optional[float]: + if value is None: + return None + try: + return float(value) / 1024.0 + except (TypeError, ValueError): + return None + + +def _fmt_window(value: Optional[float], *, precision: int = 6) -> str: + """Format an energy/duration window value for the CSV (empty string if None).""" + if value is None: + return "" + fmt = "%%.%df" % precision + return fmt % float(value) + + +def _empty_row( + *, + app: str, + variant: str, + device: str, + run_id: str, + note: str, +) -> dict: + row = {col: "" for col in COLUMNS} + row.update({ + "app": app or "unknown", + "variant": variant or "unknown", + "device": device or "unknown", + "run_id": run_id or "unknown", + "timestamp": _now_iso_utc(), + "installed": "unknown", + "appium_session": "unknown", + "workload_started": "unknown", + "scenario_action_pass": "unknown", + "scenario_strict_pass": "unknown", + "energy_source": "none", + "aggregated_energy_mwh": "", + "crash_anr_status": "unknown", + "apk_path": "", + "apk_sha256": "", + "apk_storage": "", + # E0.T7 - leave the six energy-window cells empty in the failure row. + "energy_pre_workload_mwh": "", + "energy_workload_only_mwh": "", + "energy_whole_window_mwh": "", + "duration_pre_workload_s": "", + "duration_workload_s": "", + "duration_whole_window_s": "", + # E1.5.T5 - aux sampler cells stay empty in the failure row. + "cpu_avg_pct": "", + "cpu_p95_pct": "", + "mem_pss_avg_mb": "", + "mem_pss_max_mb": "", + "notes": note, + }) + return row + + +# --------------------------------------------------------------------------- +# CSV I/O (idempotent) +# --------------------------------------------------------------------------- + + +def _read_existing_rows(matrix_path: str) -> list[dict]: + if not op.isfile(matrix_path): + return [] + try: + with open(matrix_path, "r", encoding="utf-8", newline="") as f: + reader = csv.DictReader(f) + return [dict(r) for r in reader] + except OSError: + return [] + + +def _write_rows(matrix_path: str, rows: list[dict]) -> None: + os.makedirs(op.dirname(matrix_path), exist_ok=True) + buf = io.StringIO() + writer = csv.DictWriter(buf, fieldnames=COLUMNS, extrasaction="ignore") + writer.writeheader() + for row in rows: + clean = {col: row.get(col, "") for col in COLUMNS} + writer.writerow(clean) + with open(matrix_path, "w", encoding="utf-8", newline="") as f: + f.write(buf.getvalue()) + + +def _upsert_row(matrix_path: str, new_row: dict) -> None: + rows = _read_existing_rows(matrix_path) + rows = [r for r in rows if r.get("run_id") != new_row["run_id"]] + rows.append(new_row) + _write_rows(matrix_path, rows) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Append/replace one row in specs/tracking_matrix.csv from an AndroidRunner run output dir.", + ) + parser.add_argument("--app", required=True, help="short app key, e.g. metronome") + parser.add_argument( + "--variant", + required=True, + help="one of: %s" % ", ".join(VALID_VARIANTS), + ) + parser.add_argument( + "--device", + required=True, + help="one of: %s" % ", ".join(VALID_DEVICES), + ) + parser.add_argument( + "--run-output-dir", + required=True, + help="path to the AndroidRunner run output dir (basename = run_id)", + ) + parser.add_argument( + "--run-id", + default=None, + help="override the run_id (default: basename of --run-output-dir)", + ) + parser.add_argument( + "--matrix-path", + default=None, + help="override the path to specs/tracking_matrix.csv", + ) + parser.add_argument( + "--apk-path", + default=None, + help=( + "path to the APK that produced this run; SHA-256 is computed from " + "the file. Overrides apk_meta.json. Stored relative to the " + "workspace root if it lives inside the workspace." + ), + ) + parser.add_argument( + "--apk-sha256", + default=None, + help=( + "optional pre-computed SHA-256 hex; used for cross-checking only " + "when --apk-path points to an existing file (mismatch is logged " + "to the notes column)." + ), + ) + parser.add_argument( + "--apk-storage", + default=None, + help=( + "where the APK currently lives, e.g. 'local', " + "'gh-release:/', 'zenodo:/'. " + "Defaults to 'local' when --apk-path is given." + ), + ) + return parser + + +def _main(argv: list[str]) -> int: + parser = _build_parser() + args = parser.parse_args(argv) + + matrix_path = _resolve_matrix_path(args.matrix_path) + workspace_root = _resolve_workspace_root(matrix_path) + run_output_dir = op.abspath(args.run_output_dir) + run_id = args.run_id or op.basename(run_output_dir.rstrip(os.sep)) or "unknown" + + try: + if not op.isdir(run_output_dir): + row = _empty_row( + app=args.app, + variant=args.variant, + device=args.device, + run_id=run_id, + note="update_failed: run_output_dir not found: %s" % run_output_dir, + ) + else: + row = _compose_row( + app=args.app, + variant=args.variant, + device=args.device, + run_id=run_id, + run_output_dir=run_output_dir, + workspace_root=workspace_root, + cli_apk_path=args.apk_path, + cli_apk_sha256=args.apk_sha256, + cli_apk_storage=args.apk_storage, + ) + _upsert_row(matrix_path, row) + except Exception as ex: # noqa: BLE001 - best-effort by design + try: + row = _empty_row( + app=args.app, + variant=args.variant, + device=args.device, + run_id=run_id, + note="update_failed: %s: %s" % (type(ex).__name__, ex), + ) + _upsert_row(matrix_path, row) + except Exception: # noqa: BLE001 - swallow to honor the never-raise contract + sys.stderr.write("update_tracking_matrix: failed to record failure row\n") + sys.stderr.write(traceback.format_exc()) + return 0 + + +if __name__ == "__main__": + sys.exit(_main(sys.argv[1:])) diff --git a/examples/batterymanager/_templates/app_variant_2min.json b/examples/batterymanager/_templates/app_variant_2min.json new file mode 100644 index 000000000..e963e272f --- /dev/null +++ b/examples/batterymanager/_templates/app_variant_2min.json @@ -0,0 +1,46 @@ +{ + "_comment": "Parametric Android Runner experiment template for the cross-device, cross-variant energy matrix. Materialize one concrete config per (app, variant, device) by substituting the 5 double-curly-brace placeholders defined below. Locked invariants: duration=120000ms (2 minutes), repetitions=3, interaction_covers_duration=true, time_between_run=5000. Materialized for app_id={{APP_ID}}, application_id={{APPLICATION_ID}}, apk_path={{APK_PATH}}, device={{DEVICE_NAME}}, interaction_hook={{INTERACTION_HOOK}}. Matches the structural reference monkey_espresso_mirror_2min_baseline.json (same profilers.batterymanager.* shape).", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "{{DEVICE_NAME}}": {} + }, + "repetitions": 3, + "interaction_covers_duration": true, + "paths": [ + "{{APK_PATH}}" + ], + "application_id": "{{APPLICATION_ID}}", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1 + T2 — Android Runner's documented CPU + memory profiler (Plugins/android/Android.py, A-Mobile 2020 paper §3.4). Polls `dumpsys cpuinfo` (system TOTAL pct) and `dumpsys meminfo ` (subject app PSS in KB) every sample_interval ms. Outputs land at /data///android/_.csv. Aggregates via Scripts/aux_postprocess.py (avg/p50/p95/max into aux_summary.json) consumed by update_tracking_matrix.py.", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_{{APP_ID}}.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "{{INTERACTION_HOOK}}", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/_templates/device_pixel3.json b/examples/batterymanager/_templates/device_pixel3.json new file mode 100644 index 000000000..858712fed --- /dev/null +++ b/examples/batterymanager/_templates/device_pixel3.json @@ -0,0 +1,28 @@ +{ + "_comment": "Pixel 3 device profile (Android 12 / API 31). ABI: ro.product.cpu.abilist == arm64-v8a,armeabi-v7a (both 64-bit and legacy 32-bit JNI libs install). BatteryManager: works directly via the BatteryManager companion app — no extra adb permission grant required, no sysfs fallback needed. Used as the canonical reference device for all energy comparisons. Use sysfs only if the BatteryManager companion is missing on this device.", + "device_name": "Pixel 3", + "device_serial_placeholder": "{{PIXEL3_SERIAL}}", + "android_version": "12", + "api_level": 31, + "abi": { + "primary": "arm64-v8a", + "all": ["arm64-v8a", "armeabi-v7a"], + "ro_product_cpu_abilist": "arm64-v8a,armeabi-v7a", + "accepts_armeabi_v7a_only_apks": true, + "accepts_arm64_v8a_only_apks": true + }, + "energy": { + "preferred_source": "batterymanager", + "fallback_source": "sysfs", + "requires_battery_stats_grant": "consistency-only", + "battery_stats_grant_command": "adb -s {{PIXEL3_SERIAL}} shell pm grant com.example.batterymanager_utility android.permission.BATTERY_STATS", + "battery_stats_grant_changes_current_now": false, + "battery_stats_grant_changes_current_now_evidence": "Controlled A/B test 2026-05-08 evening (runs output/2026.05.08_203238 vs 203642): mean CURRENT_NOW differs by 8% within a within-run stdev of 63% — pure noise. AOSP /frameworks/base/api/current.txt confirms BATTERY_PROPERTY_CURRENT_NOW (id=2) carries no @RequiresPermission(BATTERY_STATS) annotation; only properties 7-9 do.", + "requires_rebuilt_companion_apk": true, + "rebuilt_companion_apk_path": "_external/batterymanager-companion-fork/app/build/outputs/apk/debug/app-debug.apk", + "notes": "BatteryManager reads BATTERY_PROPERTY_CURRENT_NOW + EXTRA_VOLTAGE on Android 12 without any privileged permission. The grant is auto-applied by before_experiment as consistency hygiene only (so device_state.json carries the same battery_stats_granted=True across all 3 devices); it does NOT change the reading. Float-charge masking (§1b) is the actual measurement blocker on this device and only Epic 1.6 hub-ctrl can fix it." + }, + "android_runner_devices_block": { + "Pixel 3": {} + } +} diff --git a/examples/batterymanager/_templates/device_pixel6.json b/examples/batterymanager/_templates/device_pixel6.json new file mode 100644 index 000000000..79dd8ea5a --- /dev/null +++ b/examples/batterymanager/_templates/device_pixel6.json @@ -0,0 +1,28 @@ +{ + "_comment": "Pixel 6 device profile. ABI: ro.product.cpu.abilist == arm64-v8a (64-bit only). BatteryManager works on this device. Some intermediate Android versions tighten BATTERY_STATS, so if the BatteryManager companion throws SecurityException run: 'adb -s shell pm grant android.permission.BATTERY_STATS' (where is the BatteryManager companion APK, e.g. com.example.batterymanager_utility). If that grant is rejected, fall back to sysfs sampling. APKs whose only JNI libs are under lib/armeabi-v7a/ will fail to install with INSTALL_FAILED_NO_MATCHING_ABIS — same constraint as Pixel 9.", + "device_name": "Pixel 6", + "device_serial_placeholder": "{{PIXEL6_SERIAL}}", + "android_version": "see-adb-shell-getprop-ro.build.version.release", + "api_level": null, + "abi": { + "primary": "arm64-v8a", + "all": ["arm64-v8a"], + "ro_product_cpu_abilist": "arm64-v8a", + "accepts_armeabi_v7a_only_apks": false, + "accepts_arm64_v8a_only_apks": true + }, + "energy": { + "preferred_source": "batterymanager", + "fallback_source": "sysfs", + "requires_battery_stats_grant": "consistency-only", + "battery_stats_grant_command": "adb -s {{PIXEL6_SERIAL}} shell pm grant com.example.batterymanager_utility android.permission.BATTERY_STATS", + "battery_stats_grant_changes_current_now": false, + "battery_stats_grant_changes_current_now_evidence": "Verified 2026-05-08 evening on actual Pixel 6 (18131FDF6002S9, Android 13): grant succeeds with rebuilt APK, granted=true. AOSP source identical to Android 12/15 for CURRENT_NOW gating, so an A/B test would almost certainly show the same null effect as Pixel 3 (output/2026.05.08_203238 vs 203642).", + "requires_rebuilt_companion_apk": true, + "rebuilt_companion_apk_path": "_external/batterymanager-companion-fork/app/build/outputs/apk/debug/app-debug.apk", + "notes": "First Pixel 6 baseline (output/2026.05.08_210052) shows 100% positive current samples — same float-charge masking (§1b) as Pixel 3 / Pixel 9. The grant is consistency hygiene only, not a measurement protection. Hub-ctrl Epic 1.6 is the only canonical fix." + }, + "android_runner_devices_block": { + "Pixel 6": {} + } +} diff --git a/examples/batterymanager/_templates/device_pixel9.json b/examples/batterymanager/_templates/device_pixel9.json new file mode 100644 index 000000000..119802293 --- /dev/null +++ b/examples/batterymanager/_templates/device_pixel9.json @@ -0,0 +1,31 @@ +{ + "_comment": "Pixel 9 device profile (Android 15). CRITICAL ABI CONSTRAINT: ro.product.cpu.abilist == arm64-v8a only — Pixel 9 is 64-bit-only. APKs whose only JNI libraries live under lib/armeabi-v7a/ (e.g. some Bangcle-packed outputs) WILL fail with INSTALL_FAILED_NO_MATCHING_ABIS. This is an artifact-side mismatch, not a signing or adb bug; never weaken protection to force install — re-pack with arm64-v8a / fat-ABI shells, or mark the variant as not supported on Pixel 9 in final_dataset/. CRITICAL ENERGY CONSTRAINT: BatteryManager companion raises 'SecurityException: BATTERY_STATS permission' on Android 15. Mitigation: 'adb -s {{PIXEL9_SERIAL}} shell pm grant android.permission.BATTERY_STATS'. If the grant is denied (privileged-permission policy), fall back to sysfs sampling for this device — never silently skip. Diagnostic checks: 'adb -s {{PIXEL9_SERIAL}} shell getprop ro.product.cpu.abilist' and 'unzip -l | grep lib/' before declaring an install issue fixed.", + "device_name": "Pixel 9", + "device_serial_placeholder": "{{PIXEL9_SERIAL}}", + "android_version": "15", + "api_level": 35, + "abi": { + "primary": "arm64-v8a", + "all": ["arm64-v8a"], + "ro_product_cpu_abilist": "arm64-v8a", + "accepts_armeabi_v7a_only_apks": false, + "accepts_arm64_v8a_only_apks": true, + "incompatibility_error_when_violated": "INSTALL_FAILED_NO_MATCHING_ABIS" + }, + "energy": { + "preferred_source": "batterymanager", + "fallback_source": "sysfs", + "requires_battery_stats_grant": "consistency-only", + "battery_stats_grant_command": "adb -s {{PIXEL9_SERIAL}} shell pm grant com.example.batterymanager_utility android.permission.BATTERY_STATS", + "battery_stats_grant_changes_current_now": false, + "battery_stats_grant_changes_current_now_evidence": "Verified 2026-05-08 evening on actual Pixel 9 (56040DLAQ0027U, Android 15): grant succeeds with rebuilt APK, granted=true (i.e. Android 15 does NOT tighten the development flag relative to Android 12; previously suspected to). AOSP source identical to Android 12 for CURRENT_NOW gating; no separate A/B test was run on Pixel 9 because the API path is the same.", + "requires_rebuilt_companion_apk": true, + "rebuilt_companion_apk_path": "_external/batterymanager-companion-fork/app/build/outputs/apk/debug/app-debug.apk", + "battery_stats_grant_failure_mode_with_upstream_apk": "SecurityException: Package com.example.batterymanager_utility has not requested permission android.permission.BATTERY_STATS — this is because the upstream S2-group APK doesn't declare the permission. Install the rebuilt fork APK from rebuilt_companion_apk_path and the grant will succeed.", + "fallback_decision_rule": "If 'pm grant' against the REBUILT fork APK is ever rejected (would be new — currently works on Android 12/13/15), switch this device to sysfs sampling. The grant is consistency hygiene only, not a measurement protection — sysfs vs BatteryManager doesn't change the float-charge masking (§1b) underlying problem.", + "notes": "Earlier 'BATTERY_STATS works on Android 15' afternoon claim is RETRACTED: the grant only succeeds with the rebuilt APK, and even then doesn't change CURRENT_NOW readings. See docs/MEASUREMENT_NOISE_SOURCES.md §1 evening retraction." + }, + "android_runner_devices_block": { + "Pixel 9": {} + } +} diff --git a/examples/batterymanager/androidskk_baseline_pixel9.json b/examples/batterymanager/androidskk_baseline_pixel9.json new file mode 100644 index 000000000..222e4464f --- /dev/null +++ b/examples/batterymanager/androidskk_baseline_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + AndroidSKK baseline. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/androidskk_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/tamo_AndroidSKK/app/build/outputs/apk/debug/app-debug.apk"], + "application_id": "jp.deadend.noname.skk", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_androidskk.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_androidskk.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/androidskk_baseline_pixel9_wifi.json b/examples/batterymanager/androidskk_baseline_pixel9_wifi.json new file mode 100644 index 000000000..e0e2632ab --- /dev/null +++ b/examples/batterymanager/androidskk_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of androidskk_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + AndroidSKK baseline. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/androidskk_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/tamo_AndroidSKK/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "jp.deadend.noname.skk", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_androidskk.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_androidskk.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/androidskk_stub_bangcle_pixel9.json b/examples/batterymanager/androidskk_stub_bangcle_pixel9.json new file mode 100644 index 000000000..5f9a73e35 --- /dev/null +++ b/examples/batterymanager/androidskk_stub_bangcle_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + AndroidSKK stub-Bangcle. Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/androidskk_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Thesis/APKs/androidskk_stub_baseline_protected.signed.apk"], + "application_id": "jp.deadend.noname.skk", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_androidskk.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_androidskk.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/androidskk_stub_bangcle_pixel9_wifi.json b/examples/batterymanager/androidskk_stub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..817a55122 --- /dev/null +++ b/examples/batterymanager/androidskk_stub_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of androidskk_stub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + AndroidSKK stub-Bangcle. Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/androidskk_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/androidskk_stub_baseline_protected.signed.apk" + ], + "application_id": "jp.deadend.noname.skk", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_androidskk.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_androidskk.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/another_notes_baseline_pixel9.json b/examples/batterymanager/another_notes_baseline_pixel9.json new file mode 100644 index 000000000..49f1fdbfe --- /dev/null +++ b/examples/batterymanager/another_notes_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + maltaisn another-notes baseline (unprotected debug, v1.6.1). Pure-bytecode. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/another_notes_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/maltaisn_another-notes-app/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.maltaisn.notes.debug", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_another_notes.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_another_notes.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/another_notes_baseline_pixel9_wifi.json b/examples/batterymanager/another_notes_baseline_pixel9_wifi.json new file mode 100644 index 000000000..0b13f275c --- /dev/null +++ b/examples/batterymanager/another_notes_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of another_notes_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + maltaisn another-notes baseline (unprotected debug, v1.6.1). Pure-bytecode. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/another_notes_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/maltaisn_another-notes-app/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.maltaisn.notes.debug", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_another_notes.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_another_notes.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/another_notes_stub_bangcle_pixel9.json b/examples/batterymanager/another_notes_stub_bangcle_pixel9.json new file mode 100644 index 000000000..fd21947bc --- /dev/null +++ b/examples/batterymanager/another_notes_stub_bangcle_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + another-notes stub-Bangcle (Option D). Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/another_notes_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/another_notes_stub_baseline_protected.signed.apk" + ], + "application_id": "com.maltaisn.notes.debug", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_another_notes.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_another_notes.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/another_notes_stub_bangcle_pixel9_wifi.json b/examples/batterymanager/another_notes_stub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..5cf67c583 --- /dev/null +++ b/examples/batterymanager/another_notes_stub_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of another_notes_stub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + another-notes stub-Bangcle (Option D). Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/another_notes_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/another_notes_stub_baseline_protected.signed.apk" + ], + "application_id": "com.maltaisn.notes.debug", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_another_notes.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_another_notes.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/avnc_bangcle_pixel9.json b/examples/batterymanager/avnc_bangcle_pixel9.json new file mode 100644 index 000000000..9918da7bd --- /dev/null +++ b/examples/batterymanager/avnc_bangcle_pixel9.json @@ -0,0 +1,30 @@ +{ + "_comment": "Pixel 9 + Bangcle-packed AVNC v2.9.1 (com.gaurav.avnc.debug). Matched-pair counterpart to avnc_baseline_pixel9.json. APK source: baseline app-debug.apk uploaded to Bangcle web service 2026-05-19; protected output zipaligned + signed with ~/.android/debug.keystore. lib/arm64-v8a/libSecShell.so present (Option D not needed — baseline already had fat-ABI libnative-vnc.so). primaryCpuAbi=arm64-v8a verified at install time. APPIUM_BUILD_LABEL=bangcle for tracking-matrix tagging.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Thesis/APKs/avnc_protected.signed.apk"], + "application_id": "com.gaurav.avnc.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": ["BATTERY_PROPERTY_CURRENT_NOW", "EXTRA_VOLTAGE"], + "persistency_strategy": ["adb_log"] + }, + "android": {"sample_interval": 1000, "data_points": ["cpu", "mem"]} + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_avnc.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_avnc.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/avnc_bangcle_pixel9_wifi.json b/examples/batterymanager/avnc_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..f2f1889bc --- /dev/null +++ b/examples/batterymanager/avnc_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of avnc_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + Bangcle-packed AVNC v2.9.1 (com.gaurav.avnc.debug). Matched-pair counterpart to avnc_baseline_pixel9.json. APK source: baseline app-debug.apk uploaded to Bangcle web service 2026-05-19; protected output zipaligned + signed with ~/.android/debug.keystore. lib/arm64-v8a/libSecShell.so present (Option D not needed \u2014 baseline already had fat-ABI libnative-vnc.so). primaryCpuAbi=arm64-v8a verified at install time. APPIUM_BUILD_LABEL=bangcle for tracking-matrix tagging.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/avnc_protected.signed.apk" + ], + "application_id": "com.gaurav.avnc.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_avnc.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_avnc.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/avnc_baseline_pixel9.json b/examples/batterymanager/avnc_baseline_pixel9.json new file mode 100644 index 000000000..184d5f3eb --- /dev/null +++ b/examples/batterymanager/avnc_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + AVNC baseline (unprotected debug, v2.9.1). APK built locally via gradle :app:assembleDebug; fat-ABI (arm64-v8a + others), auto-signed with debug keystore. Use this config to: (a) generate the cross-variant baseline for energy comparison, (b) verify the per-app harness module + hooks behave correctly before running the Bangcle variant. Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/avnc_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/gujjwal00_avnc/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.gaurav.avnc.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_avnc.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_avnc.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/avnc_baseline_pixel9_wifi.json b/examples/batterymanager/avnc_baseline_pixel9_wifi.json new file mode 100644 index 000000000..beae77430 --- /dev/null +++ b/examples/batterymanager/avnc_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "WiFi-ADB validation copy of avnc_baseline_pixel9.json. Uses 'Pixel 9-W' device alias which maps to 10.15.10.93:5555 (TCP ADB over WiFi) in devices.json — same physical Pixel 9 (56040DLAQ0027U) but reached via WiFi instead of USB. Goal: prove every Appium + scenarios.py call works transparently over TCP-ADB so we can later cut USB power via uhubctl without losing the test session. APK + interaction + scripts unchanged from the USB version. Created 2026-05-19 as part of Epic 1.6 / WiFi-ADB validation.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/gujjwal00_avnc/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.gaurav.avnc.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_avnc.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_avnc.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/calculator_baseline_pixel9.json b/examples/batterymanager/calculator_baseline_pixel9.json new file mode 100644 index 000000000..dd67638f8 --- /dev/null +++ b/examples/batterymanager/calculator_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + exponentialGroth Calculator baseline (unprotected debug, v1.7.2). APK built locally via gradle :app:assembleDebug; pure-bytecode (no lib/ directory at baseline). Used to: (a) generate the cross-variant baseline for energy comparison, (b) verify the per-app harness module before running the stub-Bangcle variant. Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/calculator_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/exponentialGroth_Calculator/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.exponential_groth.calculator", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_calculator.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_calculator.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/calculator_baseline_pixel9_wifi.json b/examples/batterymanager/calculator_baseline_pixel9_wifi.json new file mode 100644 index 000000000..4d30e172c --- /dev/null +++ b/examples/batterymanager/calculator_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of calculator_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + exponentialGroth Calculator baseline (unprotected debug, v1.7.2). APK built locally via gradle :app:assembleDebug; pure-bytecode (no lib/ directory at baseline). Used to: (a) generate the cross-variant baseline for energy comparison, (b) verify the per-app harness module before running the stub-Bangcle variant. Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/calculator_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/exponentialGroth_Calculator/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.exponential_groth.calculator", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_calculator.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_calculator.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/calculator_stub_bangcle_pixel9.json b/examples/batterymanager/calculator_stub_bangcle_pixel9.json new file mode 100644 index 000000000..b761ac711 --- /dev/null +++ b/examples/batterymanager/calculator_stub_bangcle_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + Calculator stub-Bangcle variant (Option D from docs/BANGCLE_PIXEL9_ABI_COMPATIBILITY.md). APK = signed Bangcle pack of the stub-injected baseline (lib/arm64-v8a/{libplaceholder.so, libSecShell.so}). Installs cleanly on Pixel 9 as primaryCpuAbi=arm64-v8a (empirically verified 2026-05-15). Compare against calculator_baseline_pixel9.json for the cross-variant signal. Run command: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/calculator_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/calculator_stub_baseline_protected.signed.apk" + ], + "application_id": "com.exponential_groth.calculator", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_calculator.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_calculator.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/calculator_stub_bangcle_pixel9_wifi.json b/examples/batterymanager/calculator_stub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..6e506d699 --- /dev/null +++ b/examples/batterymanager/calculator_stub_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of calculator_stub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + Calculator stub-Bangcle variant (Option D from docs/BANGCLE_PIXEL9_ABI_COMPATIBILITY.md). APK = signed Bangcle pack of the stub-injected baseline (lib/arm64-v8a/{libplaceholder.so, libSecShell.so}). Installs cleanly on Pixel 9 as primaryCpuAbi=arm64-v8a (empirically verified 2026-05-15). Compare against calculator_baseline_pixel9.json for the cross-variant signal. Run command: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/calculator_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/calculator_stub_baseline_protected.signed.apk" + ], + "application_id": "com.exponential_groth.calculator", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_calculator.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_calculator.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/compute_energy_from_sysfs.py b/examples/batterymanager/compute_energy_from_sysfs.py new file mode 100644 index 000000000..b11976506 --- /dev/null +++ b/examples/batterymanager/compute_energy_from_sysfs.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +Compute power/energy from sysfs battery samples produced by Scripts/interaction.py. + +Inputs (per run): sysfs_power_*.csv with columns: + - epoch_ms + - current_now_ua (microamps, can be negative while discharging) + - voltage_now_uv (microvolts) + +Outputs: + - sysfs_energy_summary.csv (one row per input file) + - sysfs_energy_.csv (optional per-sample power; enable with --write-power-csv) +""" + +from __future__ import annotations + +import argparse +import csv +import math +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, Iterator, Optional + + +@dataclass(frozen=True) +class Sample: + t_s: float + current_a: float + voltage_v: float + + @property + def power_w(self) -> float: + # Use magnitude so discharging (-I) yields positive power draw. + return abs(self.current_a) * self.voltage_v + + +def iter_sysfs_csv_files(root: Path) -> Iterator[Path]: + for p in root.rglob("sysfs_power_*.csv"): + if p.is_file(): + yield p + + +def parse_samples(path: Path) -> list[Sample]: + samples: list[Sample] = [] + with path.open(newline="") as f: + r = csv.DictReader(f) + for row in r: + epoch_ms = int(row["epoch_ms"]) + current_ua = row.get("current_now_ua") + voltage_uv = row.get("voltage_now_uv") + if current_ua is None or voltage_uv is None: + continue + if current_ua == "" or voltage_uv == "": + continue + + current_a = float(current_ua) * 1e-6 + voltage_v = float(voltage_uv) * 1e-6 + t_s = epoch_ms / 1000.0 + samples.append(Sample(t_s=t_s, current_a=current_a, voltage_v=voltage_v)) + return samples + + +def trapz_energy_j(samples: list[Sample]) -> float: + if len(samples) < 2: + return float("nan") + e = 0.0 + for a, b in zip(samples, samples[1:]): + dt = b.t_s - a.t_s + if dt <= 0: + continue + e += 0.5 * (a.power_w + b.power_w) * dt + return e + + +def duration_s(samples: list[Sample]) -> float: + if len(samples) < 2: + return float("nan") + return max(0.0, samples[-1].t_s - samples[0].t_s) + + +def safe_mean(values: Iterable[float]) -> float: + vals = [v for v in values if v is not None and not math.isnan(v)] + return sum(vals) / len(vals) if vals else float("nan") + + +def write_power_csv(input_path: Path, samples: list[Sample]) -> Path: + out_path = input_path.with_name(input_path.stem.replace("sysfs_power_", "sysfs_energy_") + ".csv") + with out_path.open("w", newline="") as f: + w = csv.writer(f) + w.writerow(["t_s", "current_a", "voltage_v", "power_w"]) + for s in samples: + w.writerow([s.t_s, s.current_a, s.voltage_v, s.power_w]) + return out_path + + +def main(argv: Optional[list[str]] = None) -> int: + ap = argparse.ArgumentParser() + ap.add_argument( + "root", + nargs="?", + default=str(Path(__file__).resolve().parent / "output"), + help="Root directory containing Android Runner output/ (default: examples/batterymanager/output)", + ) + ap.add_argument("--write-power-csv", action="store_true", help="Write per-sample power CSV next to each input file.") + args = ap.parse_args(argv) + + root = Path(args.root).expanduser().resolve() + files = sorted(iter_sysfs_csv_files(root)) + if not files: + raise SystemExit(f"No sysfs_power_*.csv found under {root}") + + # Put summary at the root of the selected output folder (or provided root). + summary_path = root / "sysfs_energy_summary.csv" + with summary_path.open("w", newline="") as f: + w = csv.writer(f) + w.writerow( + [ + "input_file", + "n_samples", + "duration_s", + "avg_current_a_abs", + "avg_voltage_v", + "avg_power_w", + "energy_trapz_j", + ] + ) + for p in files: + samples = parse_samples(p) + dur = duration_s(samples) + e_j = trapz_energy_j(samples) + avg_i = safe_mean(abs(s.current_a) for s in samples) + avg_v = safe_mean(s.voltage_v for s in samples) + avg_p = safe_mean(s.power_w for s in samples) + + if args.write_power_csv: + write_power_csv(p, samples) + + w.writerow([str(p.relative_to(root)), len(samples), dur, avg_i, avg_v, avg_p, e_j]) + + print(f"Wrote {summary_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/examples/batterymanager/crcontainer_bangcle_pixel9.json b/examples/batterymanager/crcontainer_bangcle_pixel9.json new file mode 100644 index 000000000..f2cb207bc --- /dev/null +++ b/examples/batterymanager/crcontainer_bangcle_pixel9.json @@ -0,0 +1,37 @@ +{ + "_comment": "Pixel 9 + curiouslearning CRcontainer Bangcle variant. APK = signed pack of the upstream v2.34.2 debug baseline (versionCode 70, 4 ABIs incl. arm64-v8a + librive-android.so wrapped + libc++_shared.so wrapped; libSecShell.so added). applicationId preserved at org.curiouslearning.container. PageSizeMismatchDialog dismissed at session start. Run: APPIUM_BUILD_LABEL=bangcle python3 . examples/batterymanager/crcontainer_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/curios-reader_protected.signed.apk" + ], + "application_id": "org.curiouslearning.container", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": ["BATTERY_PROPERTY_CURRENT_NOW", "EXTRA_VOLTAGE"], + "persistency_strategy": ["adb_log"] + }, + "android": { + "sample_interval": 1000, + "data_points": ["cpu", "mem"] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_crcontainer.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_crcontainer.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/crcontainer_bangcle_pixel9_wifi.json b/examples/batterymanager/crcontainer_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..13ca4b61f --- /dev/null +++ b/examples/batterymanager/crcontainer_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of crcontainer_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + curiouslearning CRcontainer Bangcle variant. APK = signed pack of the upstream v2.34.2 debug baseline (versionCode 70, 4 ABIs incl. arm64-v8a + librive-android.so wrapped + libc++_shared.so wrapped; libSecShell.so added). applicationId preserved at org.curiouslearning.container. PageSizeMismatchDialog dismissed at session start. Run: APPIUM_BUILD_LABEL=bangcle python3 . examples/batterymanager/crcontainer_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/curios-reader_protected.signed.apk" + ], + "application_id": "org.curiouslearning.container", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_crcontainer.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_crcontainer.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/crcontainer_baseline_pixel9.json b/examples/batterymanager/crcontainer_baseline_pixel9.json new file mode 100644 index 000000000..1dc13299e --- /dev/null +++ b/examples/batterymanager/crcontainer_baseline_pixel9.json @@ -0,0 +1,37 @@ +{ + "_comment": "Pixel 9 + curiouslearning CRcontainer baseline at upstream v2.34.2 (versionCode 70). APK built from sibling worktree `curiouslearning_CRcontainer_v2.34.2/` (origin/main detached-HEAD at ab6b654); the pinned `curiouslearning_CRcontainer/` stays at v2.28 per AndroT-frozen convention (see specs/app_inclusion.json::measured_version pattern matching Metronome v2.1.1 + LinkHub v2.1.0 version-bumps). v2.34.2 ships 4 ABIs incl. arm64-v8a + librive-android.so (Rive animations added between v2.28 and v2.34.2, unlocks Pixel 9 Bangcle per docs/BANGCLE_PIXEL9_ABI_COMPATIBILITY.md). Network: hits public dev backend https://devcuriousreader.wpcomstaging.com/container_app_manifest/dev/web_app_manifest.json. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/crcontainer_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/curiouslearning_CRcontainer_v2.34.2/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "org.curiouslearning.container", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": ["BATTERY_PROPERTY_CURRENT_NOW", "EXTRA_VOLTAGE"], + "persistency_strategy": ["adb_log"] + }, + "android": { + "sample_interval": 1000, + "data_points": ["cpu", "mem"] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_crcontainer.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_crcontainer.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/crcontainer_baseline_pixel9_wifi.json b/examples/batterymanager/crcontainer_baseline_pixel9_wifi.json new file mode 100644 index 000000000..12ad5cd40 --- /dev/null +++ b/examples/batterymanager/crcontainer_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of crcontainer_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + curiouslearning CRcontainer baseline at upstream v2.34.2 (versionCode 70). APK built from sibling worktree `curiouslearning_CRcontainer_v2.34.2/` (origin/main detached-HEAD at ab6b654); the pinned `curiouslearning_CRcontainer/` stays at v2.28 per AndroT-frozen convention (see specs/app_inclusion.json::measured_version pattern matching Metronome v2.1.1 + LinkHub v2.1.0 version-bumps). v2.34.2 ships 4 ABIs incl. arm64-v8a + librive-android.so (Rive animations added between v2.28 and v2.34.2, unlocks Pixel 9 Bangcle per docs/BANGCLE_PIXEL9_ABI_COMPATIBILITY.md). Network: hits public dev backend https://devcuriousreader.wpcomstaging.com/container_app_manifest/dev/web_app_manifest.json. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/crcontainer_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/curiouslearning_CRcontainer_v2.34.2/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "org.curiouslearning.container", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_crcontainer.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_crcontainer.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/diaguard_bangcle_pixel3.json b/examples/batterymanager/diaguard_bangcle_pixel3.json new file mode 100644 index 000000000..11fd43750 --- /dev/null +++ b/examples/batterymanager/diaguard_bangcle_pixel3.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 3 + Diaguard Bangcle-packed v3.15.1. *** Update the `paths` field below with the actual packed APK path AFTER you receive it back from the prof's Bangcle pack run. *** Diaguard's baseline APK is pure-bytecode (zero `lib//` directories) — per docs/BANGCLE_PIXEL9_ABI_COMPATIBILITY.md, Bangcle's input-driven ABI propagation means the packed APK will ship `lib/armeabi-v7a/libSecShell.so` only. That's Pixel 3 / Pixel 6 compatible (both accept armv7) but not Pixel 9 (arm64-only). This is the documented Option C path: measure the (bangcle, pixel9) cell as missing, measure (bangcle, pixel3) and (bangcle, pixel6) cleanly. Sign with apksigner before running. Run command: APPIUM_BUILD_LABEL=bangcle python3 . examples/batterymanager/diaguard_bangcle_pixel3.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/diaguard_protected.signed.apk" + ], + "application_id": "com.faltenreich.diaguard", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_diaguard.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_diaguard.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/diaguard_bangcle_pixel6.json b/examples/batterymanager/diaguard_bangcle_pixel6.json new file mode 100644 index 000000000..75c0e36b8 --- /dev/null +++ b/examples/batterymanager/diaguard_bangcle_pixel6.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 6 + Diaguard Bangcle-packed v3.15.1. Sibling of diaguard_bangcle_pixel3.json with the device block swapped. Same Bangcle armv7-only output applies — Pixel 6 accepts armv7 (it's a 64-bit device that still supports 32-bit native libs, unlike Pixel 9 which is 64-bit-only). Update `paths` with the packed APK path after receiving it back. Run: APPIUM_BUILD_LABEL=bangcle python3 . examples/batterymanager/diaguard_bangcle_pixel6.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 6": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/diaguard_protected.signed.apk" + ], + "application_id": "com.faltenreich.diaguard", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_diaguard.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_diaguard.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/diaguard_baseline_pixel9.json b/examples/batterymanager/diaguard_baseline_pixel9.json new file mode 100644 index 000000000..e2d89b9a3 --- /dev/null +++ b/examples/batterymanager/diaguard_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + Diaguard baseline (unprotected debug, v3.15.1). APK was built locally via gradle :app:assembleDebug; pure bytecode (no native libs), auto-signed with debug keystore. Use this config to: (a) generate the cross-variant baseline for energy comparison, (b) verify the per-app harness module + hooks behave correctly before running the Bangcle variant. Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/diaguard_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/Faltenreich_Diaguard/app/build/outputs/apk/store/debug/app-store-debug.apk" + ], + "application_id": "com.faltenreich.diaguard", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_diaguard.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_diaguard.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/diaguard_baseline_pixel9_wifi.json b/examples/batterymanager/diaguard_baseline_pixel9_wifi.json new file mode 100644 index 000000000..299e62fcd --- /dev/null +++ b/examples/batterymanager/diaguard_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of diaguard_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + Diaguard baseline (unprotected debug, v3.15.1). APK was built locally via gradle :app:assembleDebug; pure bytecode (no native libs), auto-signed with debug keystore. Use this config to: (a) generate the cross-variant baseline for energy comparison, (b) verify the per-app harness module + hooks behave correctly before running the Bangcle variant. Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/diaguard_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/Faltenreich_Diaguard/app/build/outputs/apk/store/debug/app-store-debug.apk" + ], + "application_id": "com.faltenreich.diaguard", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_diaguard.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_diaguard.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/diaguard_stub_bangcle_pixel9.json b/examples/batterymanager/diaguard_stub_bangcle_pixel9.json new file mode 100644 index 000000000..c810f9713 --- /dev/null +++ b/examples/batterymanager/diaguard_stub_bangcle_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + Diaguard stub-Bangcle (Option D). Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/diaguard_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/diaguard_stub_baseline_protected.signed.apk" + ], + "application_id": "com.faltenreich.diaguard", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_diaguard.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_diaguard.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/diaguard_stub_bangcle_pixel9_wifi.json b/examples/batterymanager/diaguard_stub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..158abd4a4 --- /dev/null +++ b/examples/batterymanager/diaguard_stub_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of diaguard_stub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + Diaguard stub-Bangcle (Option D). Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/diaguard_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/diaguard_stub_baseline_protected.signed.apk" + ], + "application_id": "com.faltenreich.diaguard", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_diaguard.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_diaguard.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/documenter_bangcle_pixel9.json b/examples/batterymanager/documenter_bangcle_pixel9.json new file mode 100644 index 000000000..40df927ce --- /dev/null +++ b/examples/batterymanager/documenter_bangcle_pixel9.json @@ -0,0 +1,46 @@ +{ + "_comment": "Smoke run of Bangcle-packed ViliusSutkus89 Documenter v1.0.14 (debug build) on Pixel 9. Targets the .debug-suffixed package because app/build.gradle.kts:79-80 sets applicationIdSuffix = '.debug' on the debug build type. Packed APK at /home/irena/Documents/Master Thesis/APKs/documenter-universal-debug_protected.signed.apk (already signed + installable; verified on Pixel 9 at scaffold time). Energy axis: native PDF/DOC conversion via libpdf2htmlEX-android.so + libwvware-android.so in dedicated remote worker processes (:pdf2htmlEXWorker, :wvWareWorker). Locked invariants from _templates/app_variant_2min.json kept (duration=120000ms profiler-window ceiling, interaction_covers_duration=true, time_between_run=5000). repetitions=1 for smoke; bump to 3 after baseline smoke is clean. NB: openDocumentAttempt scenario will only exercise the native-conversion energy axis if a PDF or DOC fixture is selectable via the SAF picker — push e.g. app_repositories_newest/final_dataset/ViliusSutkus89_Documenter/app/src/androidTest/assets/testFiles/geneve_1564.pdf to /sdcard/Download/sample.pdf before the run.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/documenter-universal-debug_protected.signed.apk" + ], + "application_id": "com.viliussutkus89.documenter.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1+T2 CPU + memory polling", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_documenter.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_documenter.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/documenter_bangcle_pixel9_wifi.json b/examples/batterymanager/documenter_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..d6aa84f7c --- /dev/null +++ b/examples/batterymanager/documenter_bangcle_pixel9_wifi.json @@ -0,0 +1,46 @@ +{ + "_comment": "[WIFI-MODE COPY of documenter_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Smoke run of Bangcle-packed ViliusSutkus89 Documenter v1.0.14 (debug build) on Pixel 9. Targets the .debug-suffixed package because app/build.gradle.kts:79-80 sets applicationIdSuffix = '.debug' on the debug build type. Packed APK at /home/irena/Documents/Master Thesis/APKs/documenter-universal-debug_protected.signed.apk (already signed + installable; verified on Pixel 9 at scaffold time). Energy axis: native PDF/DOC conversion via libpdf2htmlEX-android.so + libwvware-android.so in dedicated remote worker processes (:pdf2htmlEXWorker, :wvWareWorker). Locked invariants from _templates/app_variant_2min.json kept (duration=120000ms profiler-window ceiling, interaction_covers_duration=true, time_between_run=5000). repetitions=1 for smoke; bump to 3 after baseline smoke is clean. NB: openDocumentAttempt scenario will only exercise the native-conversion energy axis if a PDF or DOC fixture is selectable via the SAF picker \u2014 push e.g. app_repositories_newest/final_dataset/ViliusSutkus89_Documenter/app/src/androidTest/assets/testFiles/geneve_1564.pdf to /sdcard/Download/sample.pdf before the run.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/documenter-universal-debug_protected.signed.apk" + ], + "application_id": "com.viliussutkus89.documenter.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1+T2 CPU + memory polling", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_documenter.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_documenter.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/documenter_baseline_pixel9.json b/examples/batterymanager/documenter_baseline_pixel9.json new file mode 100644 index 000000000..ae8379f82 --- /dev/null +++ b/examples/batterymanager/documenter_baseline_pixel9.json @@ -0,0 +1,46 @@ +{ + "_comment": "Baseline (unpacked debug build) smoke of ViliusSutkus89 Documenter v1.0.14 on Pixel 9. Matched-pair counterpart to documenter_bangcle_pixel9.json. Uses arm64-v8a split APK because Pixel 9 is 64-bit only (Tensor G4). applicationIdSuffix='.debug' from app/build.gradle.kts:79-80.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/ViliusSutkus89_Documenter/app/build/outputs/apk/debug/app-arm64-v8a-debug.apk" + ], + "application_id": "com.viliussutkus89.documenter.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1+T2 CPU + memory polling", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_documenter.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_documenter.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/documenter_baseline_pixel9_wifi.json b/examples/batterymanager/documenter_baseline_pixel9_wifi.json new file mode 100644 index 000000000..610d0b6d1 --- /dev/null +++ b/examples/batterymanager/documenter_baseline_pixel9_wifi.json @@ -0,0 +1,46 @@ +{ + "_comment": "[WIFI-MODE COPY of documenter_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Baseline (unpacked debug build) smoke of ViliusSutkus89 Documenter v1.0.14 on Pixel 9. Matched-pair counterpart to documenter_bangcle_pixel9.json. Uses arm64-v8a split APK because Pixel 9 is 64-bit only (Tensor G4). applicationIdSuffix='.debug' from app/build.gradle.kts:79-80.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/ViliusSutkus89_Documenter/app/build/outputs/apk/debug/app-arm64-v8a-debug.apk" + ], + "application_id": "com.viliussutkus89.documenter.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1+T2 CPU + memory polling", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_documenter.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_documenter.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/espresso_compare_apk_paths.json b/examples/batterymanager/espresso_compare_apk_paths.json new file mode 100644 index 000000000..c06846c63 --- /dev/null +++ b/examples/batterymanager/espresso_compare_apk_paths.json @@ -0,0 +1,6 @@ +{ + "readme": "Edit these paths, then copy each value into the matching experiment JSON under \"paths\", or keep paths in the monkey_* JSON files in sync manually.", + "baseline_apk": "/CHANGE_ME/path/to/app-debug.apk", + "obfuscated_apk": "/CHANGE_ME/path/to/obfuscated-metronome.apk", + "packed_apk": "/home/irena/Documents/Master Thesis/APKs/app-protected-signed.apk" +} diff --git a/examples/batterymanager/gallerywall_baseline_pixel9.json b/examples/batterymanager/gallerywall_baseline_pixel9.json new file mode 100644 index 000000000..39bb822db --- /dev/null +++ b/examples/batterymanager/gallerywall_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + bossly GalleryWall baseline (unprotected debug, v2.3.0). Pure-bytecode. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/gallerywall_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/bossly_gallerywall/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.baysoft.gallerywall.dev", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_gallerywall.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_gallerywall.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/gallerywall_stub_bangcle_pixel9.json b/examples/batterymanager/gallerywall_stub_bangcle_pixel9.json new file mode 100644 index 000000000..d6888a40f --- /dev/null +++ b/examples/batterymanager/gallerywall_stub_bangcle_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + GalleryWall stub-Bangcle variant (Option D). Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/gallerywall_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/gallerywall_stub_baseline_protected.signed.apk" + ], + "application_id": "com.baysoft.gallerywall.dev", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_gallerywall.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_gallerywall.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/horoscapp_bangcle_pixel9.json b/examples/batterymanager/horoscapp_bangcle_pixel9.json new file mode 100644 index 000000000..df7fae516 --- /dev/null +++ b/examples/batterymanager/horoscapp_bangcle_pixel9.json @@ -0,0 +1,37 @@ +{ + "_comment": "Pixel 9 + angelsoft071 HoroscApp Bangcle variant. APK = signed pack of the v1.0.2 baseline (4 ABIs incl. arm64-v8a, libimage_processing_util_jni.so from CameraX wrapped; libSecShell.so added for all 4 ABIs). applicationId preserved at com.angelsoft.horoscapp. PageSizeMismatchDialog dismissed at session start via _dismiss_page_size_dialog_permanently. CAMERA permission auto-granted by interaction_appium_horoscapp.py before scenarios. Run: APPIUM_BUILD_LABEL=bangcle python3 . examples/batterymanager/horoscapp_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/horoscope_protected.signed.apk" + ], + "application_id": "com.angelsoft.horoscapp", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": ["BATTERY_PROPERTY_CURRENT_NOW", "EXTRA_VOLTAGE"], + "persistency_strategy": ["adb_log"] + }, + "android": { + "sample_interval": 1000, + "data_points": ["cpu", "mem"] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_horoscapp.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_horoscapp.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/horoscapp_bangcle_pixel9_wifi.json b/examples/batterymanager/horoscapp_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..099c9501b --- /dev/null +++ b/examples/batterymanager/horoscapp_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of horoscapp_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + angelsoft071 HoroscApp Bangcle variant. APK = signed pack of the v1.0.2 baseline (4 ABIs incl. arm64-v8a, libimage_processing_util_jni.so from CameraX wrapped; libSecShell.so added for all 4 ABIs). applicationId preserved at com.angelsoft.horoscapp. PageSizeMismatchDialog dismissed at session start via _dismiss_page_size_dialog_permanently. CAMERA permission auto-granted by interaction_appium_horoscapp.py before scenarios. Run: APPIUM_BUILD_LABEL=bangcle python3 . examples/batterymanager/horoscapp_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/horoscope_protected.signed.apk" + ], + "application_id": "com.angelsoft.horoscapp", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_horoscapp.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_horoscapp.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/horoscapp_baseline_pixel9.json b/examples/batterymanager/horoscapp_baseline_pixel9.json new file mode 100644 index 000000000..5ff4185a4 --- /dev/null +++ b/examples/batterymanager/horoscapp_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + angelsoft071 HoroscApp baseline (unprotected debug, v1.0.2). APK built locally; 4 ABIs (libimage_processing_util_jni.so from CameraX), auto-signed with debug keystore. Workload uses INTERNET (hits public newastro.vercel.app) + CAMERA (pre-granted by interaction_appium_horoscapp.py before scenarios run). Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/horoscapp_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/angelsoft071_HoroscApp/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.angelsoft.horoscapp", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_horoscapp.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_horoscapp.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/horoscapp_baseline_pixel9_wifi.json b/examples/batterymanager/horoscapp_baseline_pixel9_wifi.json new file mode 100644 index 000000000..fc74b83a6 --- /dev/null +++ b/examples/batterymanager/horoscapp_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of horoscapp_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + angelsoft071 HoroscApp baseline (unprotected debug, v1.0.2). APK built locally; 4 ABIs (libimage_processing_util_jni.so from CameraX), auto-signed with debug keystore. Workload uses INTERNET (hits public newastro.vercel.app) + CAMERA (pre-granted by interaction_appium_horoscapp.py before scenarios run). Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/horoscapp_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/angelsoft071_HoroscApp/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.angelsoft.horoscapp", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_horoscapp.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_horoscapp.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/iamspeed_baseline_pixel9.json b/examples/batterymanager/iamspeed_baseline_pixel9.json new file mode 100644 index 000000000..5ee86884f --- /dev/null +++ b/examples/batterymanager/iamspeed_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + ViliusSutkus89 IamSpeed baseline (unprotected debug, v1.2.5). APK built locally via gradle :app:assembleDebug; pure-bytecode (no lib/ directory at baseline). applicationId carries .debug suffix. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/iamspeed_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/ViliusSutkus89_IamSpeed/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.viliussutkus89.iamspeed.debug", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_iamspeed.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_iamspeed.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/iamspeed_stub_bangcle_pixel9.json b/examples/batterymanager/iamspeed_stub_bangcle_pixel9.json new file mode 100644 index 000000000..ccc778971 --- /dev/null +++ b/examples/batterymanager/iamspeed_stub_bangcle_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + IamSpeed stub-Bangcle variant (Option D from docs/BANGCLE_PIXEL9_ABI_COMPATIBILITY.md). APK = signed Bangcle pack of the stub-injected baseline (lib/arm64-v8a/{libplaceholder.so, libSecShell.so}). Installs cleanly as primaryCpuAbi=arm64-v8a (verified 2026-05-15). Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/iamspeed_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/iamspeed_stub_baseline_protected.signed.apk" + ], + "application_id": "com.viliussutkus89.iamspeed.debug", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_iamspeed.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_iamspeed.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/itsok_baseline_pixel9.json b/examples/batterymanager/itsok_baseline_pixel9.json new file mode 100644 index 000000000..464792bf9 --- /dev/null +++ b/examples/batterymanager/itsok_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + qubacy ItsOK baseline (unprotected debug, v0.0.1). Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/itsok_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/qubacy_ItsOK/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.qubacy.itsok", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_itsok.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_itsok.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/itsok_baseline_pixel9_wifi.json b/examples/batterymanager/itsok_baseline_pixel9_wifi.json new file mode 100644 index 000000000..e5ac11ba1 --- /dev/null +++ b/examples/batterymanager/itsok_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of itsok_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + qubacy ItsOK baseline (unprotected debug, v0.0.1). Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/itsok_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/qubacy_ItsOK/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.qubacy.itsok", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_itsok.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_itsok.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/itsok_stub_bangcle_pixel9.json b/examples/batterymanager/itsok_stub_bangcle_pixel9.json new file mode 100644 index 000000000..19422ecb0 --- /dev/null +++ b/examples/batterymanager/itsok_stub_bangcle_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + ItsOK stub-Bangcle (Option D, apostrophe-stripped label variant). Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/itsok_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/itsok_stub_baseline_label_v2_protected.signed.apk" + ], + "application_id": "com.qubacy.itsok", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_itsok.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_itsok.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/itsok_stub_bangcle_pixel9_wifi.json b/examples/batterymanager/itsok_stub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..736ad3231 --- /dev/null +++ b/examples/batterymanager/itsok_stub_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of itsok_stub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + ItsOK stub-Bangcle (Option D, apostrophe-stripped label variant). Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/itsok_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/itsok_stub_baseline_label_v2_protected.signed.apk" + ], + "application_id": "com.qubacy.itsok", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_itsok.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_itsok.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/keep_recipe_baseline_pixel9.json b/examples/batterymanager/keep_recipe_baseline_pixel9.json new file mode 100644 index 000000000..b52557a21 --- /dev/null +++ b/examples/batterymanager/keep_recipe_baseline_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + Keep-Recipe baseline. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/keep_recipe_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/bwakessa_Keep-Recipe/app/build/outputs/apk/debug/app-debug.apk"], + "application_id": "com.keeprecipes.android", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_keep_recipe.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_keep_recipe.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/keep_recipe_baseline_pixel9_wifi.json b/examples/batterymanager/keep_recipe_baseline_pixel9_wifi.json new file mode 100644 index 000000000..e77c43ad1 --- /dev/null +++ b/examples/batterymanager/keep_recipe_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of keep_recipe_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + Keep-Recipe baseline. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/keep_recipe_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/bwakessa_Keep-Recipe/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.keeprecipes.android", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_keep_recipe.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_keep_recipe.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/keep_recipe_stub_bangcle_pixel9.json b/examples/batterymanager/keep_recipe_stub_bangcle_pixel9.json new file mode 100644 index 000000000..2e47962d8 --- /dev/null +++ b/examples/batterymanager/keep_recipe_stub_bangcle_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + Keep-Recipe stub-Bangcle. Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/keep_recipe_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Thesis/APKs/keep_recipe_stub_baseline_protected.signed.apk"], + "application_id": "com.keeprecipes.android", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_keep_recipe.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_keep_recipe.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/keep_recipe_stub_bangcle_pixel9_wifi.json b/examples/batterymanager/keep_recipe_stub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..67ed103f5 --- /dev/null +++ b/examples/batterymanager/keep_recipe_stub_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of keep_recipe_stub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + Keep-Recipe stub-Bangcle. Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/keep_recipe_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/keep_recipe_stub_baseline_protected.signed.apk" + ], + "application_id": "com.keeprecipes.android", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_keep_recipe.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_keep_recipe.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/keepitup_baseline_pixel9.json b/examples/batterymanager/keepitup_baseline_pixel9.json new file mode 100644 index 000000000..6868e85cf --- /dev/null +++ b/examples/batterymanager/keepitup_baseline_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + keepitup baseline.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/ibbaa_keepitup/keepitupmain/build/outputs/apk/debug/keepitup-debug.apk"], + "application_id": "net.ibbaa.keepitup", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_keepitup.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_keepitup.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/keepitup_baseline_pixel9_wifi.json b/examples/batterymanager/keepitup_baseline_pixel9_wifi.json new file mode 100644 index 000000000..68ac1c73d --- /dev/null +++ b/examples/batterymanager/keepitup_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of keepitup_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + keepitup baseline.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/ibbaa_keepitup/keepitupmain/build/outputs/apk/debug/keepitup-debug.apk" + ], + "application_id": "net.ibbaa.keepitup", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_keepitup.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_keepitup.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/keepitup_stub_bangcle_pixel9.json b/examples/batterymanager/keepitup_stub_bangcle_pixel9.json new file mode 100644 index 000000000..76a924a3a --- /dev/null +++ b/examples/batterymanager/keepitup_stub_bangcle_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + keepitup stub-Bangcle.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Thesis/APKs/keepitup_stub_baseline_protected.signed.apk"], + "application_id": "net.ibbaa.keepitup", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_keepitup.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_keepitup.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/keepitup_stub_bangcle_pixel9_wifi.json b/examples/batterymanager/keepitup_stub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..67ee75f50 --- /dev/null +++ b/examples/batterymanager/keepitup_stub_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of keepitup_stub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + keepitup stub-Bangcle.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/keepitup_stub_baseline_protected.signed.apk" + ], + "application_id": "net.ibbaa.keepitup", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_keepitup.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_keepitup.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/linkhub_bangcle_pixel9.json b/examples/batterymanager/linkhub_bangcle_pixel9.json new file mode 100644 index 000000000..21b51e0bf --- /dev/null +++ b/examples/batterymanager/linkhub_bangcle_pixel9.json @@ -0,0 +1,46 @@ +{ + "_comment": "Smoke run of Bangcle-packed LinkHub v2.1.0 on Pixel 9. Targets the manifest package com.amrdeveloper.linkhub (no .debug suffix on this app — confirmed via aapt earlier; LinkHub's build.gradle does NOT set applicationIdSuffix). Packed APK at /home/irena/Documents/Master Thesis/APKs/Linkhub_protected.signed.apk (already signed and installed on Pixel 9). Locked invariants from _templates/app_variant_2min.json kept (duration=120000ms interpreted as profiler-window ceiling, interaction_covers_duration=true, time_between_run=5000). repetitions=1 for smoke; bump to 3 after baseline smoke is clean. v2.1.0 Compose re-derive landed 2026-05-12 — scenarios use contentDescription / text / className+instance selectors (no testTags in the Compose source).", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/Linkhub_protected.signed.apk" + ], + "application_id": "com.amrdeveloper.linkhub", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1+T2 CPU + memory polling", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_linkhub.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_linkhub.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/linkhub_bangcle_pixel9_wifi.json b/examples/batterymanager/linkhub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..a2abe4206 --- /dev/null +++ b/examples/batterymanager/linkhub_bangcle_pixel9_wifi.json @@ -0,0 +1,46 @@ +{ + "_comment": "[WIFI-MODE COPY of linkhub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Smoke run of Bangcle-packed LinkHub v2.1.0 on Pixel 9. Targets the manifest package com.amrdeveloper.linkhub (no .debug suffix on this app \u2014 confirmed via aapt earlier; LinkHub's build.gradle does NOT set applicationIdSuffix). Packed APK at /home/irena/Documents/Master Thesis/APKs/Linkhub_protected.signed.apk (already signed and installed on Pixel 9). Locked invariants from _templates/app_variant_2min.json kept (duration=120000ms interpreted as profiler-window ceiling, interaction_covers_duration=true, time_between_run=5000). repetitions=1 for smoke; bump to 3 after baseline smoke is clean. v2.1.0 Compose re-derive landed 2026-05-12 \u2014 scenarios use contentDescription / text / className+instance selectors (no testTags in the Compose source).", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/Linkhub_protected.signed.apk" + ], + "application_id": "com.amrdeveloper.linkhub", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1+T2 CPU + memory polling", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_linkhub.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_linkhub.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/linkhub_baseline_pixel3.json b/examples/batterymanager/linkhub_baseline_pixel3.json new file mode 100644 index 000000000..b51f96e83 --- /dev/null +++ b/examples/batterymanager/linkhub_baseline_pixel3.json @@ -0,0 +1,46 @@ +{ + "_comment": "AmrDeveloper LinkHub baseline experiment for Pixel 3, materialized from _templates/app_variant_2min.json. APK path is PROVISIONAL — user must build via './gradlew assembleDebug' from app_repositories_newest/final_dataset/AmrDeveloper_LinkHub before the smoke run. Locked invariants: duration=120000ms (interpreted as profiler-window ceiling per CONVENTIONS.md §6 post-2026-05-08), repetitions=3, interaction_covers_duration=true, time_between_run=5000. Workload runs the locked LINKHUB_SCENARIO_LIST end-to-end (per-app fixed-N contract); wall time is a measured output bounded by the 120s ceiling.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 3, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/AmrDeveloper_LinkHub/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.amrdeveloper.linkhub", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1 + T2 — Android Runner's documented CPU + memory profiler (Plugins/android/Android.py, A-Mobile 2020 paper §3.4). Polls dumpsys cpuinfo (system TOTAL pct) and dumpsys meminfo (subject app PSS in KB) every sample_interval ms. Outputs land at /data///android/_.csv. Aggregates via Scripts/aux_postprocess.py (avg/p50/p95/max into aux_summary.json) consumed by update_tracking_matrix.py.", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_linkhub.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_linkhub.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/linkhub_baseline_pixel9.json b/examples/batterymanager/linkhub_baseline_pixel9.json new file mode 100644 index 000000000..0d1d46ca2 --- /dev/null +++ b/examples/batterymanager/linkhub_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Baseline (unpacked debug build) smoke of AmrDeveloper LinkHub v2.1.0 on Pixel 9. Matched-pair counterpart to linkhub_bangcle_pixel9.json.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/AmrDeveloper_LinkHub/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.amrdeveloper.linkhub", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_linkhub.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_linkhub.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/linkhub_baseline_pixel9_wifi.json b/examples/batterymanager/linkhub_baseline_pixel9_wifi.json new file mode 100644 index 000000000..f7cc0528a --- /dev/null +++ b/examples/batterymanager/linkhub_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of linkhub_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Baseline (unpacked debug build) smoke of AmrDeveloper LinkHub v2.1.0 on Pixel 9. Matched-pair counterpart to linkhub_bangcle_pixel9.json.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/AmrDeveloper_LinkHub/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.amrdeveloper.linkhub", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_linkhub.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_linkhub.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/medtimer_baseline_pixel9.json b/examples/batterymanager/medtimer_baseline_pixel9.json new file mode 100644 index 000000000..b0bf571a8 --- /dev/null +++ b/examples/batterymanager/medtimer_baseline_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + MedTimer baseline.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/Futsch1_medTimer/app/build/outputs/apk/debug/MedTimer-debug.apk"], + "application_id": "com.futsch1.medtimer", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_medtimer.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_medtimer.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/medtimer_baseline_pixel9_wifi.json b/examples/batterymanager/medtimer_baseline_pixel9_wifi.json new file mode 100644 index 000000000..92de0c806 --- /dev/null +++ b/examples/batterymanager/medtimer_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of medtimer_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + MedTimer baseline.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/Futsch1_medTimer/app/build/outputs/apk/debug/MedTimer-debug.apk" + ], + "application_id": "com.futsch1.medtimer", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_medtimer.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_medtimer.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/medtimer_stub_bangcle_pixel9.json b/examples/batterymanager/medtimer_stub_bangcle_pixel9.json new file mode 100644 index 000000000..a40467c29 --- /dev/null +++ b/examples/batterymanager/medtimer_stub_bangcle_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + MedTimer stub-Bangcle.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Thesis/APKs/medtimer_stub_baseline_protected.signed.apk"], + "application_id": "com.futsch1.medtimer", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_medtimer.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_medtimer.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/medtimer_stub_bangcle_pixel9_wifi.json b/examples/batterymanager/medtimer_stub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..cfeb2ce2b --- /dev/null +++ b/examples/batterymanager/medtimer_stub_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of medtimer_stub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + MedTimer stub-Bangcle.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/medtimer_stub_baseline_protected.signed.apk" + ], + "application_id": "com.futsch1.medtimer", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_medtimer.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_medtimer.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/metronome_baidu_pixel9.json b/examples/batterymanager/metronome_baidu_pixel9.json new file mode 100644 index 000000000..256ec4cc8 --- /dev/null +++ b/examples/batterymanager/metronome_baidu_pixel9.json @@ -0,0 +1,30 @@ +{ + "_comment": "Pixel 9 + Baidu-packed Metronome (com.bobek.metronome). Counterpart to metronome_bangcle_pixel9.json — tests a different commercial obfuscation vendor (Baidu's `libbaiduprotect.so` runtime guard instead of Bangcle's `libSecShell.so`). APK already aligned + signed externally (filename ends `-aligned-debugSigned.apk`); all 4 ABIs including arm64-v8a are present so no Option-D stub-injection is needed. APPIUM_BUILD_LABEL=baidu for tracking-matrix tagging — this adds a NEW variant column alongside baseline / bangcle.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Thesis/APKs/metronome_baidu_packed-aligned-debugSigned.apk"], + "application_id": "com.bobek.metronome", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": ["BATTERY_PROPERTY_CURRENT_NOW", "EXTRA_VOLTAGE"], + "persistency_strategy": ["adb_log"] + }, + "android": {"sample_interval": 1000, "data_points": ["cpu", "mem"]} + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_metronome.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/metronome_bangcle_pixel3.json b/examples/batterymanager/metronome_bangcle_pixel3.json new file mode 100644 index 000000000..9f5bf7d90 --- /dev/null +++ b/examples/batterymanager/metronome_bangcle_pixel3.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 3 + Bangcle-packed Metronome v2.1.1 (versionCode 26). Sibling of metronome_bangcle_pixel9.json; same APK (it has all 4 ABIs: arm64-v8a + armeabi-v7a + x86 + x86_64). Pixel 3 selects armeabi-v7a or arm64-v8a at install time (both are supported on Android 12). Used to verify cross-device compatibility of the same packed binary AND to smoke-test the Compose testTag fallback patch in _lib/scoring.py (post-2026-05-12). Float-charge masking applies to Pixel 3 as well — energy column will be tagged energy_invalid_usb_supplying / energy_invalid_charge_dominant until Epic 1.6 hub-ctrl is in place.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/com.bobek.metronome_26_protected-v2.1.1.signed.apk" + ], + "application_id": "com.bobek.metronome", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_metronome.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/metronome_bangcle_pixel9.json b/examples/batterymanager/metronome_bangcle_pixel9.json new file mode 100644 index 000000000..396770652 --- /dev/null +++ b/examples/batterymanager/metronome_bangcle_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + Bangcle-packed Metronome v2.1.1 (versionCode 26, native libs for arm64-v8a/armeabi-v7a/x86/x86_64, signed with local debug keystore). Counterpart of monkey_espresso_mirror_2min_baseline_pixel9.json but tests the Bangcle variant instead of unprotected debug. APK source: prof packed upstream github.com/bobek/Metronome v2.1.1 release with Bangcle and shipped it unsigned; we zipalign+sign with ~/.android/debug.keystore (apksigner v2+v3). before_experiment hook chains: apply_device_state -> grant BATTERY_STATS to companion (E0.T10 auto-grant covers com.example.batterymanager_utility) -> uninstall com.bobek.metronome. AndroidRunner then installs the APK from `paths` before run 0. Espresso-mirror Set alpha (N=8) drives the workload via Scripts/interaction_appium_metronome_espresso_mirror.py — Bangcle preserves resource IDs, so the package:id/* selectors and content-descriptions from v1.7.2 should still resolve; protected (libSecShell) hooks fire in the native audio path during playFastSubdivisions and tempoSweepPlay. Recommended env for the run: APPIUM_BUILD_LABEL=bangcle for tracking-matrix tagging.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/com.bobek.metronome_26_protected-v2.1.1.signed.apk" + ], + "application_id": "com.bobek.metronome", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_metronome.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/metronome_bangcle_pixel9_wifi.json b/examples/batterymanager/metronome_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..607c1647b --- /dev/null +++ b/examples/batterymanager/metronome_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of metronome_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + Bangcle-packed Metronome v2.1.1 (versionCode 26, native libs for arm64-v8a/armeabi-v7a/x86/x86_64, signed with local debug keystore). Counterpart of monkey_espresso_mirror_2min_baseline_pixel9.json but tests the Bangcle variant instead of unprotected debug. APK source: prof packed upstream github.com/bobek/Metronome v2.1.1 release with Bangcle and shipped it unsigned; we zipalign+sign with ~/.android/debug.keystore (apksigner v2+v3). before_experiment hook chains: apply_device_state -> grant BATTERY_STATS to companion (E0.T10 auto-grant covers com.example.batterymanager_utility) -> uninstall com.bobek.metronome. AndroidRunner then installs the APK from `paths` before run 0. Espresso-mirror Set alpha (N=8) drives the workload via Scripts/interaction_appium_metronome_espresso_mirror.py \u2014 Bangcle preserves resource IDs, so the package:id/* selectors and content-descriptions from v1.7.2 should still resolve; protected (libSecShell) hooks fire in the native audio path during playFastSubdivisions and tempoSweepPlay. Recommended env for the run: APPIUM_BUILD_LABEL=bangcle for tracking-matrix tagging.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/com.bobek.metronome_26_protected-v2.1.1.signed.apk" + ], + "application_id": "com.bobek.metronome", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_metronome.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_espresso_mirror_2min_baseline.json b/examples/batterymanager/monkey_espresso_mirror_2min_baseline.json new file mode 100644 index 000000000..6c185c957 --- /dev/null +++ b/examples/batterymanager/monkey_espresso_mirror_2min_baseline.json @@ -0,0 +1,44 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/app-debug.apk" + ], + "application_id": "com.bobek.metronome", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_metronome.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_espresso_mirror_2min_baseline_pixel6.json b/examples/batterymanager/monkey_espresso_mirror_2min_baseline_pixel6.json new file mode 100644 index 000000000..ee75b35df --- /dev/null +++ b/examples/batterymanager/monkey_espresso_mirror_2min_baseline_pixel6.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 6 baseline Metronome run. Identical to monkey_espresso_mirror_2min_baseline.json EXCEPT the device block points to Pixel 6 instead of Pixel 3. Pixel 6 is arm64-v8a only — the Bangcle Metronome APK (armeabi-v7a only) WILL NOT install here, so this config only references the multi-arch baseline APK. If the BatteryManager companion fails with SecurityException on Pixel 6, run: adb -s shell pm grant com.example.batterymanager_utility android.permission.BATTERY_STATS", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 6": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/app-debug.apk" + ], + "application_id": "com.bobek.metronome", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_metronome.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_espresso_mirror_2min_baseline_pixel9.json b/examples/batterymanager/monkey_espresso_mirror_2min_baseline_pixel9.json new file mode 100644 index 000000000..008869ed4 --- /dev/null +++ b/examples/batterymanager/monkey_espresso_mirror_2min_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 baseline Metronome run. Identical to monkey_espresso_mirror_2min_baseline.json EXCEPT the device block points to Pixel 9. Pixel 9 is arm64-v8a only, so the Bangcle Metronome APK (armeabi-v7a only) WILL NOT install here — same constraint as Pixel 6. ALSO: Pixel 9's Android version tightens BATTERY_STATS permissions; the BatteryManager companion APK MUST have the permission granted before this run, OR the BatteryManager profiler will throw SecurityException and report zero/garbage values. Pre-flight: adb -s shell pm grant com.example.batterymanager_utility android.permission.BATTERY_STATS", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/app-debug.apk" + ], + "application_id": "com.bobek.metronome", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_metronome.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_espresso_mirror_2min_obfuscated.json b/examples/batterymanager/monkey_espresso_mirror_2min_obfuscated.json new file mode 100644 index 000000000..b99ba265d --- /dev/null +++ b/examples/batterymanager/monkey_espresso_mirror_2min_obfuscated.json @@ -0,0 +1,44 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/CHANGE_ME/path/to/obfuscated-metronome.apk" + ], + "application_id": "com.bobek.metronome", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_metronome.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_espresso_mirror_2min_packed.json b/examples/batterymanager/monkey_espresso_mirror_2min_packed.json new file mode 100644 index 000000000..0cbcd0941 --- /dev/null +++ b/examples/batterymanager/monkey_espresso_mirror_2min_packed.json @@ -0,0 +1,44 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/app-protected-signed.apk" + ], + "application_id": "com.bobek.metronome", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_metronome.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_experiment.json b/examples/batterymanager/monkey_experiment.json new file mode 100644 index 000000000..a5e03c6a0 --- /dev/null +++ b/examples/batterymanager/monkey_experiment.json @@ -0,0 +1,24 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 2, + "apps": [ + "com.bobek.metronome" + ], + "duration": 60000, + "profilers": {}, + "scripts": { + "before_experiment": "Scripts/before_experiment.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} + diff --git a/examples/batterymanager/monkey_experiment_pixel3.json b/examples/batterymanager/monkey_experiment_pixel3.json new file mode 100644 index 000000000..6b98f62f6 --- /dev/null +++ b/examples/batterymanager/monkey_experiment_pixel3.json @@ -0,0 +1,23 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 2, + "apps": [ + "com.bobek.metronome" + ], + "duration": 60000, + "profilers": {}, + "scripts": { + "before_experiment": "Scripts/before_experiment.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_experiment_pixel3_appium_espresso_mirror_packed_apk.json b/examples/batterymanager/monkey_experiment_pixel3_appium_espresso_mirror_packed_apk.json new file mode 100644 index 000000000..252363197 --- /dev/null +++ b/examples/batterymanager/monkey_experiment_pixel3_appium_espresso_mirror_packed_apk.json @@ -0,0 +1,37 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/app-protected-signed.apk" + ], + "application_id": "com.bobek.metronome", + "duration": 600000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_metronome.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_experiment_pixel3_appium_metronome.json b/examples/batterymanager/monkey_experiment_pixel3_appium_metronome.json new file mode 100644 index 000000000..fde9720f9 --- /dev/null +++ b/examples/batterymanager/monkey_experiment_pixel3_appium_metronome.json @@ -0,0 +1,24 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 2, + "interaction_covers_duration": true, + "apps": [ + "com.bobek.metronome" + ], + "duration": 120000, + "profilers": {}, + "scripts": { + "before_experiment": "Scripts/before_experiment.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_experiment_pixel3_appium_metronome_batterymanager.json b/examples/batterymanager/monkey_experiment_pixel3_appium_metronome_batterymanager.json new file mode 100644 index 000000000..d16f9196f --- /dev/null +++ b/examples/batterymanager/monkey_experiment_pixel3_appium_metronome_batterymanager.json @@ -0,0 +1,36 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "apps": [ + "com.bobek.metronome" + ], + "duration": 60000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_experiment_pixel3_appium_metronome_batterymanager_apk.json b/examples/batterymanager/monkey_experiment_pixel3_appium_metronome_batterymanager_apk.json new file mode 100644 index 000000000..297f2b49e --- /dev/null +++ b/examples/batterymanager/monkey_experiment_pixel3_appium_metronome_batterymanager_apk.json @@ -0,0 +1,36 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 2, + "interaction_covers_duration": true, + "paths": [ + "/absolute/path/to/com.bobek.metronome.apk" + ], + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_experiment_pixel3_appium_metronome_espresso_mirror.json b/examples/batterymanager/monkey_experiment_pixel3_appium_metronome_espresso_mirror.json new file mode 100644 index 000000000..61f803fa0 --- /dev/null +++ b/examples/batterymanager/monkey_experiment_pixel3_appium_metronome_espresso_mirror.json @@ -0,0 +1,36 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "apps": [ + "com.bobek.metronome" + ], + "duration": 60000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_metronome_espresso_mirror.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/monkey_experiment_pixel3_batterymanager.json b/examples/batterymanager/monkey_experiment_pixel3_batterymanager.json new file mode 100644 index 000000000..252162dff --- /dev/null +++ b/examples/batterymanager/monkey_experiment_pixel3_batterymanager.json @@ -0,0 +1,35 @@ +{ + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 2, + "apps": [ + "com.bobek.metronome" + ], + "duration": 60000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_monkey_only.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/pdfviewer_bangcle_pixel9.json b/examples/batterymanager/pdfviewer_bangcle_pixel9.json new file mode 100644 index 000000000..fb2689674 --- /dev/null +++ b/examples/batterymanager/pdfviewer_bangcle_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + afreakyelf Pdf-Viewer Bangcle variant. APK = signed pack of the v1.1 debug baseline (4 ABIs incl. arm64-v8a, libandroidx.graphics.path.so wrapped; libSecShell.so added for all 4 ABIs). Installs cleanly on Pixel 9 (Bangcle Pixel 9 verdict empirically confirmed YES for this app). applicationId preserved at com.rajat.sample.pdfviewer (no debug suffix in baseline gradle). Compare strict-UI pass against baseline run for the cross-variant signal. Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=bangcle python3 . examples/batterymanager/pdfviewer_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/pdfviewer_protected.signed.apk" + ], + "application_id": "com.rajat.sample.pdfviewer", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_pdfviewer.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_pdfviewer.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/pdfviewer_bangcle_pixel9_wifi.json b/examples/batterymanager/pdfviewer_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..2d8bb87f8 --- /dev/null +++ b/examples/batterymanager/pdfviewer_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of pdfviewer_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + afreakyelf Pdf-Viewer Bangcle variant. APK = signed pack of the v1.1 debug baseline (4 ABIs incl. arm64-v8a, libandroidx.graphics.path.so wrapped; libSecShell.so added for all 4 ABIs). Installs cleanly on Pixel 9 (Bangcle Pixel 9 verdict empirically confirmed YES for this app). applicationId preserved at com.rajat.sample.pdfviewer (no debug suffix in baseline gradle). Compare strict-UI pass against baseline run for the cross-variant signal. Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=bangcle python3 . examples/batterymanager/pdfviewer_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/pdfviewer_protected.signed.apk" + ], + "application_id": "com.rajat.sample.pdfviewer", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_pdfviewer.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_pdfviewer.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/pdfviewer_baseline_pixel9.json b/examples/batterymanager/pdfviewer_baseline_pixel9.json new file mode 100644 index 000000000..4bca0599e --- /dev/null +++ b/examples/batterymanager/pdfviewer_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + afreakyelf Pdf-Viewer baseline (unprotected debug, v1.1). APK built locally via gradle :app:assembleDebug; 4 ABIs (Compose-graphics native lib libandroidx.graphics.path.so present), auto-signed with debug keystore. Use this config to: (a) generate the cross-variant baseline for energy comparison, (b) verify the per-app harness module + hooks behave correctly before running the Bangcle variant. Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/pdfviewer_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/afreakyelf_Pdf-Viewer/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.rajat.sample.pdfviewer", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_pdfviewer.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_pdfviewer.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/pdfviewer_baseline_pixel9_wifi.json b/examples/batterymanager/pdfviewer_baseline_pixel9_wifi.json new file mode 100644 index 000000000..e39f7d852 --- /dev/null +++ b/examples/batterymanager/pdfviewer_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of pdfviewer_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + afreakyelf Pdf-Viewer baseline (unprotected debug, v1.1). APK built locally via gradle :app:assembleDebug; 4 ABIs (Compose-graphics native lib libandroidx.graphics.path.so present), auto-signed with debug keystore. Use this config to: (a) generate the cross-variant baseline for energy comparison, (b) verify the per-app harness module + hooks behave correctly before running the Bangcle variant. Run command (from android-runner/ with venv active): APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/pdfviewer_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/afreakyelf_Pdf-Viewer/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.rajat.sample.pdfviewer", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_pdfviewer.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_pdfviewer.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/podaura_bangcle_pixel9.json b/examples/batterymanager/podaura_bangcle_pixel9.json new file mode 100644 index 000000000..ff775a53d --- /dev/null +++ b/examples/batterymanager/podaura_bangcle_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + SkyD666 PodAura Bangcle-protected matched-pair counterpart. APK source: arm64-v8a split (PodAura_3.2-alpha11_arm64-v8a_debug_GitHub.apk, ~60 MB baseline) uploaded to Bangcle web service 2026-05-19; protected output zipaligned + signed with ~/.android/debug.keystore. lib/arm64-v8a/{libavcodec.so, libavfilter.so, libmpv.so, libtorrent4j.so, libxml2.so, libplayer.so, libandroidx.graphics.path.so, libc++_shared.so, libdatastore_shared_counter.so, libSecShell.so} all preserved (Bangcle propagated libSecShell.so into the arm64 dir, did NOT modify or strip any of the input native libs). primaryCpuAbi=arm64-v8a verified at install time. APPIUM_BUILD_LABEL=bangcle for tracking-matrix tagging.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Thesis/APKs/PodAura_3.2-alpha11_arm64-v8a_debug_GitHub_protected.signed.apk"], + "application_id": "com.skyd.anivu.debug", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_podaura.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_podaura.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/podaura_bangcle_pixel9_wifi.json b/examples/batterymanager/podaura_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..0c3fe0143 --- /dev/null +++ b/examples/batterymanager/podaura_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of podaura_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + SkyD666 PodAura Bangcle-protected matched-pair counterpart. APK source: arm64-v8a split (PodAura_3.2-alpha11_arm64-v8a_debug_GitHub.apk, ~60 MB baseline) uploaded to Bangcle web service 2026-05-19; protected output zipaligned + signed with ~/.android/debug.keystore. lib/arm64-v8a/{libavcodec.so, libavfilter.so, libmpv.so, libtorrent4j.so, libxml2.so, libplayer.so, libandroidx.graphics.path.so, libc++_shared.so, libdatastore_shared_counter.so, libSecShell.so} all preserved (Bangcle propagated libSecShell.so into the arm64 dir, did NOT modify or strip any of the input native libs). primaryCpuAbi=arm64-v8a verified at install time. APPIUM_BUILD_LABEL=bangcle for tracking-matrix tagging.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/PodAura_3.2-alpha11_arm64-v8a_debug_GitHub_protected.signed.apk" + ], + "application_id": "com.skyd.anivu.debug", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_podaura.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_podaura.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/podaura_baseline_pixel9.json b/examples/batterymanager/podaura_baseline_pixel9.json new file mode 100644 index 000000000..b8d9f96c5 --- /dev/null +++ b/examples/batterymanager/podaura_baseline_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + SkyD666 PodAura baseline. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/podaura_baseline_pixel9.json . PodAura is a Compose-based RSS / podcast / video reader. arm64-v8a split APK because Pixel 9 is 64-bit only. Internet IS used at runtime (RSS feed fetch, video stream) but the locked scenarios exercise UI nav + Compose recomposition only — energy axis is tab transitions + LazyColumn fling, no network calls in the scenario set.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/SkyD666_PodAura/app/build/outputs/apk/GitHub/debug/PodAura_3.2-alpha11_arm64-v8a_debug_GitHub.apk"], + "application_id": "com.skyd.anivu.debug", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_podaura.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_podaura.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/podaura_baseline_pixel9_wifi.json b/examples/batterymanager/podaura_baseline_pixel9_wifi.json new file mode 100644 index 000000000..c6f2db0c5 --- /dev/null +++ b/examples/batterymanager/podaura_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of podaura_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + SkyD666 PodAura baseline. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/podaura_baseline_pixel9.json . PodAura is a Compose-based RSS / podcast / video reader. arm64-v8a split APK because Pixel 9 is 64-bit only. Internet IS used at runtime (RSS feed fetch, video stream) but the locked scenarios exercise UI nav + Compose recomposition only \u2014 energy axis is tab transitions + LazyColumn fling, no network calls in the scenario set.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/SkyD666_PodAura/app/build/outputs/apk/GitHub/debug/PodAura_3.2-alpha11_arm64-v8a_debug_GitHub.apk" + ], + "application_id": "com.skyd.anivu.debug", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_podaura.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_podaura.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/poetskingdom_baseline_pixel9.json b/examples/batterymanager/poetskingdom_baseline_pixel9.json new file mode 100644 index 000000000..c937cc044 --- /dev/null +++ b/examples/batterymanager/poetskingdom_baseline_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + PoetsKingdom baseline. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/poetskingdom_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/tinochings_PoetsKingdom/app/build/outputs/apk/debug/app-debug.apk"], + "application_id": "com.wendorochena.poetskingdom", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation": "default", "sample_interval": 100, + "data_points": ["BATTERY_PROPERTY_CURRENT_NOW", "EXTRA_VOLTAGE"], "persistency_strategy": ["adb_log"]}, + "android": {"sample_interval": 1000, "data_points": ["cpu", "mem"]} + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_poetskingdom.py", + "before_run": "Scripts/before_run.py", "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_poetskingdom.py", + "before_close": "Scripts/before_close.py", "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/poetskingdom_baseline_pixel9_wifi.json b/examples/batterymanager/poetskingdom_baseline_pixel9_wifi.json new file mode 100644 index 000000000..2f4512f0b --- /dev/null +++ b/examples/batterymanager/poetskingdom_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of poetskingdom_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + PoetsKingdom baseline. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/poetskingdom_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/tinochings_PoetsKingdom/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.wendorochena.poetskingdom", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_poetskingdom.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_poetskingdom.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/poetskingdom_stub_bangcle_pixel9.json b/examples/batterymanager/poetskingdom_stub_bangcle_pixel9.json new file mode 100644 index 000000000..7560d59ac --- /dev/null +++ b/examples/batterymanager/poetskingdom_stub_bangcle_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + PoetsKingdom stub-Bangcle. Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/poetskingdom_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Thesis/APKs/poets_kingdom_stub_baseline_protected.signed.apk"], + "application_id": "com.wendorochena.poetskingdom", + "duration": 150000, + "profilers": { + "batterymanager": {"experiment_aggregation": "default", "sample_interval": 100, + "data_points": ["BATTERY_PROPERTY_CURRENT_NOW", "EXTRA_VOLTAGE"], "persistency_strategy": ["adb_log"]}, + "android": {"sample_interval": 1000, "data_points": ["cpu", "mem"]} + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_poetskingdom.py", + "before_run": "Scripts/before_run.py", "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_poetskingdom.py", + "before_close": "Scripts/before_close.py", "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/poetskingdom_stub_bangcle_pixel9_wifi.json b/examples/batterymanager/poetskingdom_stub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..4667c4c21 --- /dev/null +++ b/examples/batterymanager/poetskingdom_stub_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of poetskingdom_stub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + PoetsKingdom stub-Bangcle. Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/poetskingdom_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/poets_kingdom_stub_baseline_protected.signed.apk" + ], + "application_id": "com.wendorochena.poetskingdom", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_poetskingdom.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_poetskingdom.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/repertoire_baseline_pixel3.json b/examples/batterymanager/repertoire_baseline_pixel3.json new file mode 100644 index 000000000..06d46b4a1 --- /dev/null +++ b/examples/batterymanager/repertoire_baseline_pixel3.json @@ -0,0 +1,46 @@ +{ + "_comment": "Materialized from _templates/app_variant_2min.json for klalumiere Repertoire baseline on Pixel 3. PROVISIONAL: paths. must be built (`./gradlew :app:assembleDebug` inside app_repositories_newest/final_dataset/klalumiere_Repertoire/) and a markdown fixture pre-pushed to /sdcard/Download/happybirthday.md before the smoke run (see appium_android_tests/repertoire/CALIBRATION.md). Locked invariants preserved: duration=120000ms (failsafe ceiling under E0.T9 fixed-N contract), repetitions=3, interaction_covers_duration=true, time_between_run=5000.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 3, + "interaction_covers_duration": true, + "paths": [ + "app_repositories_newest/final_dataset/klalumiere_Repertoire/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "klalumiere.repertoire", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1 + T2 — Android Runner's documented CPU + memory profiler (Plugins/android/Android.py, A-Mobile 2020 paper §3.4). Polls `dumpsys cpuinfo` (system TOTAL pct) and `dumpsys meminfo ` (subject app PSS in KB) every sample_interval ms.", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_repertoire.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_repertoire.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/repertoire_baseline_pixel9.json b/examples/batterymanager/repertoire_baseline_pixel9.json new file mode 100644 index 000000000..f2f856290 --- /dev/null +++ b/examples/batterymanager/repertoire_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + klalumiere Repertoire baseline (unprotected debug, v2.1.2). Pre-push /sdcard/Download/happybirthday.md fixture before running. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/repertoire_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/klalumiere_Repertoire/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "klalumiere.repertoire", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_repertoire.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_repertoire.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/repertoire_baseline_pixel9_wifi.json b/examples/batterymanager/repertoire_baseline_pixel9_wifi.json new file mode 100644 index 000000000..de21aa07a --- /dev/null +++ b/examples/batterymanager/repertoire_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of repertoire_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + klalumiere Repertoire baseline (unprotected debug, v2.1.2). Pre-push /sdcard/Download/happybirthday.md fixture before running. Run: APPIUM_BUILD_LABEL=baseline python3 . examples/batterymanager/repertoire_baseline_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/klalumiere_Repertoire/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "klalumiere.repertoire", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_repertoire.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_repertoire.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/repertoire_stub_bangcle_pixel9.json b/examples/batterymanager/repertoire_stub_bangcle_pixel9.json new file mode 100644 index 000000000..55436edb6 --- /dev/null +++ b/examples/batterymanager/repertoire_stub_bangcle_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Pixel 9 + Repertoire stub-Bangcle (Option D). Pre-push /sdcard/Download/happybirthday.md fixture. Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/repertoire_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/repertoire_stub_baseline_protected.signed.apk" + ], + "application_id": "klalumiere.repertoire", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_repertoire.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_repertoire.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/repertoire_stub_bangcle_pixel9_wifi.json b/examples/batterymanager/repertoire_stub_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..f4815238c --- /dev/null +++ b/examples/batterymanager/repertoire_stub_bangcle_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of repertoire_stub_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + Repertoire stub-Bangcle (Option D). Pre-push /sdcard/Download/happybirthday.md fixture. Run: APPIUM_BUILD_LABEL=stub_bangcle python3 . examples/batterymanager/repertoire_stub_bangcle_pixel9.json", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/repertoire_stub_baseline_protected.signed.apk" + ], + "application_id": "klalumiere.repertoire", + "duration": 150000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_repertoire.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_repertoire.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/tipuous_bangcle_pixel9.json b/examples/batterymanager/tipuous_bangcle_pixel9.json new file mode 100644 index 000000000..9d95099f0 --- /dev/null +++ b/examples/batterymanager/tipuous_bangcle_pixel9.json @@ -0,0 +1,46 @@ +{ + "_comment": "Smoke run of Bangcle-packed Tipuous v1.30-debug on Pixel 9. Targets the .debug-suffixed package because that is what build.gradle.kts:46 emits (applicationIdSuffix '.debug'). Packed APK at /home/irena/Documents/Master Thesis/APKs/Linkhub... wait, tipuous_protected.signed.apk. Locked invariants from _templates/app_variant_2min.json kept (duration=120000ms, interaction_covers_duration=true, time_between_run=5000). repetitions=1 for smoke; bump to 3 after baseline smoke is clean.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/tipuous_protected.signed.apk" + ], + "application_id": "com.tips.tipuous.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1+T2 CPU + memory polling", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_tipuous.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_tipuous.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/tipuous_bangcle_pixel9_wifi.json b/examples/batterymanager/tipuous_bangcle_pixel9_wifi.json new file mode 100644 index 000000000..de116d110 --- /dev/null +++ b/examples/batterymanager/tipuous_bangcle_pixel9_wifi.json @@ -0,0 +1,46 @@ +{ + "_comment": "[WIFI-MODE COPY of tipuous_bangcle_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Smoke run of Bangcle-packed Tipuous v1.30-debug on Pixel 9. Targets the .debug-suffixed package because that is what build.gradle.kts:46 emits (applicationIdSuffix '.debug'). Packed APK at /home/irena/Documents/Master Thesis/APKs/Linkhub... wait, tipuous_protected.signed.apk. Locked invariants from _templates/app_variant_2min.json kept (duration=120000ms, interaction_covers_duration=true, time_between_run=5000). repetitions=1 for smoke; bump to 3 after baseline smoke is clean.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Thesis/APKs/tipuous_protected.signed.apk" + ], + "application_id": "com.tips.tipuous.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1+T2 CPU + memory polling", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_tipuous.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_tipuous.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/tipuous_baseline_pixel3.json b/examples/batterymanager/tipuous_baseline_pixel3.json new file mode 100644 index 000000000..881df0956 --- /dev/null +++ b/examples/batterymanager/tipuous_baseline_pixel3.json @@ -0,0 +1,46 @@ +{ + "_comment": "Materialized from _templates/app_variant_2min.json for app_id=tipuous, application_id=com.tips.tipuous, apk_path=app_repositories_newest/final_dataset/JoshLudahl_tipuous/app/build/outputs/apk/debug/app-debug.apk, device=Pixel 3, interaction_hook=Scripts/interaction_appium_tipuous.py. Locked invariants: duration=120000ms (2-minute profiler ceiling — workload itself is bounded by the 7-scenario TIPUOUS_SCENARIO_LIST end-to-end), repetitions=3, interaction_covers_duration=true, time_between_run=5000. PROVISIONAL: APK must be built first via `./gradlew assembleDebug` from app_repositories_newest/final_dataset/JoshLudahl_tipuous/. Note: debug APK manifest package is `com.tips.tipuous.debug` due to `applicationIdSuffix = '.debug'` in app/build.gradle.kts:46 — to keep application_id=com.tips.tipuous, build a release-style APK or update both `application_id` here and `PACKAGE` in Scripts/before_experiment_uninstall_tipuous.py to `com.tips.tipuous.debug`.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 3": {} + }, + "repetitions": 3, + "interaction_covers_duration": true, + "paths": [ + "app_repositories_newest/final_dataset/JoshLudahl_tipuous/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.tips.tipuous", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "_comment": "E1.5.T1 + T2 — Android Runner's documented CPU + memory profiler (Plugins/android/Android.py, A-Mobile 2020 paper §3.4). Polls `dumpsys cpuinfo` (system TOTAL pct) and `dumpsys meminfo ` (subject app PSS in KB) every sample_interval ms.", + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_tipuous.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_tipuous.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/tipuous_baseline_pixel9.json b/examples/batterymanager/tipuous_baseline_pixel9.json new file mode 100644 index 000000000..51b9da7eb --- /dev/null +++ b/examples/batterymanager/tipuous_baseline_pixel9.json @@ -0,0 +1,45 @@ +{ + "_comment": "Baseline (unpacked debug build) smoke of JoshLudahl Tipuous (tip calculator) on Pixel 9. Matched-pair counterpart to tipuous_bangcle_pixel9.json.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/JoshLudahl_tipuous/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.tips.tipuous.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_tipuous.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_tipuous.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/tipuous_baseline_pixel9_wifi.json b/examples/batterymanager/tipuous_baseline_pixel9_wifi.json new file mode 100644 index 000000000..b88c926d1 --- /dev/null +++ b/examples/batterymanager/tipuous_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of tipuous_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Baseline (unpacked debug build) smoke of JoshLudahl Tipuous (tip calculator) on Pixel 9. Matched-pair counterpart to tipuous_bangcle_pixel9.json.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/JoshLudahl_tipuous/app/build/outputs/apk/debug/app-debug.apk" + ], + "application_id": "com.tips.tipuous.debug", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_tipuous.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_tipuous.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/ytalarm_baseline_pixel9.json b/examples/batterymanager/ytalarm_baseline_pixel9.json new file mode 100644 index 000000000..e930cb89b --- /dev/null +++ b/examples/batterymanager/ytalarm_baseline_pixel9.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pixel 9 + turtton YtAlarm baseline. arm64-v8a split APK because Pixel 9 is 64-bit only. YtAlarm is a YouTube alarm clock with Chaquopy embedded Python. Public YouTube only (no server we'd host). lib//libpython.so already populated → no Option D needed for Bangcle.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": {"Pixel 9": {}}, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": ["/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/turtton_YtAlarm/app/build/outputs/apk/debug/app-arm64-v8a-debug.apk"], + "application_id": "net.turtton.ytalarm", + "duration": 120000, + "profilers": { + "batterymanager": {"experiment_aggregation":"default","sample_interval":100, + "data_points":["BATTERY_PROPERTY_CURRENT_NOW","EXTRA_VOLTAGE"],"persistency_strategy":["adb_log"]}, + "android": {"sample_interval":1000,"data_points":["cpu","mem"]} + }, + "scripts": { + "before_experiment":"Scripts/before_experiment_uninstall_ytalarm.py", + "before_run":"Scripts/before_run.py","after_launch":"Scripts/after_launch.py", + "interaction":"Scripts/interaction_appium_ytalarm.py", + "before_close":"Scripts/before_close.py","after_run":"Scripts/after_run.py", + "after_experiment":"Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/examples/batterymanager/ytalarm_baseline_pixel9_wifi.json b/examples/batterymanager/ytalarm_baseline_pixel9_wifi.json new file mode 100644 index 000000000..537a16d29 --- /dev/null +++ b/examples/batterymanager/ytalarm_baseline_pixel9_wifi.json @@ -0,0 +1,45 @@ +{ + "_comment": "[WIFI-MODE COPY of ytalarm_baseline_pixel9.json, auto-generated by tools/generate_wifi_configs.py. Original USB-mode config has \"Pixel 9\" alias; this copy uses \"Pixel 9-W\" mapped to 10.15.10.93:5555 in devices.json so the run is reachable after `uhubctl -a 0` cuts USB power.] Pixel 9 + turtton YtAlarm baseline. arm64-v8a split APK because Pixel 9 is 64-bit only. YtAlarm is a YouTube alarm clock with Chaquopy embedded Python. Public YouTube only (no server we'd host). lib//libpython.so already populated \u2192 no Option D needed for Bangcle.", + "type": "native", + "adb_path": "/home/irena/Android/Sdk/platform-tools/adb", + "devices": { + "Pixel 9-W": {} + }, + "repetitions": 1, + "interaction_covers_duration": true, + "paths": [ + "/home/irena/Documents/Master Experiment/app_repositories_newest/final_dataset/turtton_YtAlarm/app/build/outputs/apk/debug/app-arm64-v8a-debug.apk" + ], + "application_id": "net.turtton.ytalarm", + "duration": 120000, + "profilers": { + "batterymanager": { + "experiment_aggregation": "default", + "sample_interval": 100, + "data_points": [ + "BATTERY_PROPERTY_CURRENT_NOW", + "EXTRA_VOLTAGE" + ], + "persistency_strategy": [ + "adb_log" + ] + }, + "android": { + "sample_interval": 1000, + "data_points": [ + "cpu", + "mem" + ] + } + }, + "scripts": { + "before_experiment": "Scripts/before_experiment_uninstall_ytalarm.py", + "before_run": "Scripts/before_run.py", + "after_launch": "Scripts/after_launch.py", + "interaction": "Scripts/interaction_appium_ytalarm.py", + "before_close": "Scripts/before_close.py", + "after_run": "Scripts/after_run.py", + "after_experiment": "Scripts/after_experiment.py" + }, + "time_between_run": 5000 +} diff --git a/requirements-appium.txt b/requirements-appium.txt new file mode 100644 index 000000000..4afa37e8f --- /dev/null +++ b/requirements-appium.txt @@ -0,0 +1,5 @@ +# Optional: Appium workload hook (examples/batterymanager/Scripts/interaction_appium_metronome.py) +# Install on top of requirements.txt: +# pip install -r requirements.txt -r requirements-appium.txt + +Appium-Python-Client>=5.0