diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 299727b..c44a29c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,8 +86,8 @@ jobs: from PyInstaller.utils.hooks import collect_all; from openadb.version import ACBRIDGE_APK_FILENAME, RELEASE_EXE_FILENAME; assert callable(collect_all); - assert ACBRIDGE_APK_FILENAME == 'ACBridge-3.0.0.apk'; - assert RELEASE_EXE_FILENAME == 'OpenADB-3.0.0.exe'" + assert ACBRIDGE_APK_FILENAME == 'ACBridge-3.0.1.apk'; + assert RELEASE_EXE_FILENAME == 'OpenADB-3.0.1.exe'" - name: Check repository privacy guardrails shell: pwsh diff --git a/.github/workflows/device-lab.yml b/.github/workflows/device-lab.yml index 262bb1f..6c1ac75 100644 --- a/.github/workflows/device-lab.yml +++ b/.github/workflows/device-lab.yml @@ -132,7 +132,7 @@ jobs: $null -eq $json -or $json.schema -ne "openadb.device-lab.v1" -or $json.product.name -ne "OpenADB" -or - $json.product.version -ne "3.0.0" -or + $json.product.version -ne "3.0.1" -or $json.product.source_commit -ne $expectedSourceCommit -or [string]::IsNullOrWhiteSpace([string]$json.environment.os) -or [string]::IsNullOrWhiteSpace([string]$json.environment.release) -or diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 817801c..2bf781a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,15 @@ -name: OpenADB 3.0.0 release +name: OpenADB 3.0.1 release on: push: tags: - - v3.0.0 + - v3.0.1 workflow_dispatch: inputs: tag: description: Release tag; dispatch this workflow from the same tag ref required: true - default: v3.0.0 + default: v3.0.1 type: string allow_unsigned_stable: description: Explicitly publish an unsigned stable release (artifact keeps the -unsigned suffix) @@ -31,7 +31,7 @@ jobs: tag: ${{ steps.release-ref.outputs.tag }} allow_unsigned_stable: ${{ steps.release-ref.outputs.allow_unsigned_stable }} steps: - - name: Require the immutable 3.0.0 tag + - name: Require the immutable 3.0.1 tag id: release-ref shell: pwsh env: @@ -47,8 +47,8 @@ jobs: '${{ github.ref_name }}' } - if ($tag -ne 'v3.0.0') { - throw "This release workflow is intentionally restricted to v3.0.0; received '$tag'." + if ($tag -ne 'v3.0.1') { + throw "This release workflow is intentionally restricted to v3.0.1; received '$tag'." } if ($env:CURRENT_REF -ne "refs/tags/$tag") { throw "Run a manual release from the $tag tag ref, not from a branch or a different tag." @@ -212,7 +212,7 @@ jobs: if (@(Compare-Object $requiredTopLevel $actualTopLevel).Count -ne 0) { throw 'BUILD_STATUS.json has missing or unexpected top-level fields.' } - if ($status.version -ne '3.0.0') { + if ($status.version -ne '3.0.1') { throw "Unexpected build version '$($status.version)'." } if ($status.signed -isnot [bool]) { @@ -236,21 +236,21 @@ jobs: $bridgeKeys = @($status.acbridge.PSObject.Properties.Name) if (@(Compare-Object @('package', 'version_name', 'version_code', 'apk_filename') $bridgeKeys).Count -ne 0 -or $status.acbridge.package -ne 'com.communism420.acbridge' -or - $status.acbridge.version_name -ne '3.0.0' -or - [int]$status.acbridge.version_code -ne 30002 -or - $status.acbridge.apk_filename -ne 'ACBridge-3.0.0.apk') { + $status.acbridge.version_name -ne '3.0.1' -or + [int]$status.acbridge.version_code -ne 30101 -or + $status.acbridge.apk_filename -ne 'ACBridge-3.0.1.apk') { throw 'BUILD_STATUS.json contains unexpected ACBridge metadata.' } $expectedName = if ($status.signed) { - 'OpenADB-3.0.0.exe' + 'OpenADB-3.0.1.exe' } else { - 'OpenADB-3.0.0-unsigned.exe' + 'OpenADB-3.0.1-unsigned.exe' } $expectedArtifactName = if ($status.signed) { - 'OpenADB-3.0.0-windows-signed' + 'OpenADB-3.0.1-windows-signed' } else { - 'OpenADB-3.0.0-windows-unsigned' + 'OpenADB-3.0.1-windows-unsigned' } if ($env:BUILD_ARTIFACT_NAME -ne $expectedArtifactName -or $env:BUILD_OUTPUT_FILENAME -ne $expectedName -or @@ -261,7 +261,7 @@ jobs: if ($status.filename -ne $expectedName) { throw "Build filename '$($status.filename)' does not match its signed status." } - $executables = @(Get-ChildItem -LiteralPath $release -File -Filter 'OpenADB-3.0.0*.exe') + $executables = @(Get-ChildItem -LiteralPath $release -File -Filter 'OpenADB-3.0.1*.exe') if ($executables.Count -ne 1 -or $executables[0].Name -ne $expectedName) { throw 'The build artifact must contain exactly one correctly named OpenADB executable.' } @@ -303,7 +303,7 @@ jobs: throw "Unsigned build has an unexpected Authenticode state: $($signature.Status)." } - $apkSource = 'openadb/resources/acbridge/ACBridge-3.0.0.apk' + $apkSource = 'openadb/resources/acbridge/ACBridge-3.0.1.apk' if (-not (Test-Path -LiteralPath $apkSource -PathType Leaf) -or (Get-Item -LiteralPath $apkSource).Length -le 0) { throw 'The versioned ACBridge APK is missing or empty at the release tag.' @@ -312,7 +312,7 @@ jobs: $releaseFiles = @( $exe.FullName, - (Join-Path $release 'ACBridge-3.0.0.apk'), + (Join-Path $release 'ACBridge-3.0.1.apk'), $statusPath ) $newSums = foreach ($file in $releaseFiles) { @@ -344,10 +344,10 @@ jobs: $changelog = Get-Content -LiteralPath 'CHANGELOG_EN.md' -Raw -Encoding utf8 $match = [regex]::Match( $changelog, - '(?ms)^## \[3\.0\.0\].*?(?=^## \[|\z)' + '(?ms)^## \[3\.0\.1\].*?(?=^## \[|\z)' ) if (-not $match.Success) { - throw 'CHANGELOG_EN.md does not contain a 3.0.0 release section.' + throw 'CHANGELOG_EN.md does not contain a 3.0.1 release section.' } $status = Get-Content -LiteralPath 'release/BUILD_STATUS.json' -Raw -Encoding utf8 | ConvertFrom-Json @@ -360,7 +360,7 @@ jobs: } $notes = @" - # OpenADB 3.0.0 + # OpenADB 3.0.1 ## Artifact status @@ -419,11 +419,11 @@ jobs: } $title = if ($env:SIGNED -eq 'true') { - 'OpenADB 3.0.0' + 'OpenADB 3.0.1' } elseif ($env:STABLE -eq 'true') { - 'OpenADB 3.0.0 (explicitly approved unsigned)' + 'OpenADB 3.0.1 (explicitly approved unsigned)' } else { - 'OpenADB 3.0.0 unsigned preview' + 'OpenADB 3.0.1 unsigned preview' } $arguments = @( 'release', 'create', $env:RELEASE_TAG, @@ -436,7 +436,7 @@ jobs: } $arguments += @( (Join-Path 'release' $env:EXE_FILENAME), - 'release/ACBridge-3.0.0.apk', + 'release/ACBridge-3.0.1.apk', 'release/BUILD_STATUS.json', 'release/SHA256SUMS.txt' ) diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 1d710ef..7cbdaf8 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -25,7 +25,7 @@ on: required: false push: tags: - - "v3.0.0" + - "v3.0.1" permissions: contents: read @@ -39,7 +39,7 @@ defaults: shell: pwsh env: - OPENADB_VERSION: "3.0.0" + OPENADB_VERSION: "3.0.1" PLATFORM_TOOLS_VERSION: "37.0.0" PLATFORM_TOOLS_ARCHIVE: "platform-tools_r37.0.0-win.zip" PLATFORM_TOOLS_ARCHIVE_SHA1: "f29bfb58d0d6f9a57d7dbcba6cc259f9ca6f58f1" diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb7e05..00b4e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,50 @@ All notable OpenADB changes made since the start of the local audit and project redesign are documented in this file. The format is based on Keep a Changelog. The current public project version is -3.0.0. +3.0.1. -## [3.0.0] — Unreleased +## [3.0.1] — Unreleased + +### Fixed + +- Fixed all PC-to-Android P2P uploads failing before the first file byte when + Android parsed the streamed bootstrap redirection outside ACBridge's + `run-as` shell. The complete nested command is now preserved as the single + remote `adb shell` argument, so the one-time request is written inside the + helper's private files directory. +- Made bootstrap directory preparation fail immediately if its private files + directory cannot be created instead of allowing a later generic write + failure. +- P2P bootstrap failures now prefer safely redacted Android `stderr` over the + generic `Command failed with exit code 1` status, retaining the actionable + cause without exposing the request ID or bootstrap secret. + +### Version + +- Updated OpenADB, ACBridge, Windows metadata, build workflows, screenshots, + and active release documentation to version 3.0.1. +- ACBridge 3.0.1 uses `versionCode 30101` under the documented version-code + policy and is rebuilt from source as `ACBridge-3.0.1.apk`. + +### Validation + +- Added regressions that require the nested `run-as ... sh -c` script to stay + in one `adb shell` argument and that preserve useful, redacted Android error + details when bootstrap creation fails. +- Passed all 39 isolated test modules and all 565 tests with + `ResourceWarning` treated as an error, plus compileall, Ruff, workflow YAML, + embedded PowerShell, immutable Action-reference, and diff validation. +- Rebuilt and independently verified both byte-identical 45,613-byte ACBridge + APK aliases: package/version metadata, ZIP alignment, v1/v2/v3 signatures, + and the established signer digest all pass. +- Regenerated and validated the seven versioned README screenshots. The + contextual Applications capture now uses the complete offscreen render path + and every persistent navigation row is checked. +- The failure was confirmed from a sanitized local device log, but no ADB + transport remained connected for a real post-fix phone transfer. No + hardware-transfer success is claimed by this changelog. + +## [3.0.0] — 2026-07-13 ### Version diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index bbb7e05..00b4e29 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -4,9 +4,50 @@ All notable OpenADB changes made since the start of the local audit and project redesign are documented in this file. The format is based on Keep a Changelog. The current public project version is -3.0.0. +3.0.1. -## [3.0.0] — Unreleased +## [3.0.1] — Unreleased + +### Fixed + +- Fixed all PC-to-Android P2P uploads failing before the first file byte when + Android parsed the streamed bootstrap redirection outside ACBridge's + `run-as` shell. The complete nested command is now preserved as the single + remote `adb shell` argument, so the one-time request is written inside the + helper's private files directory. +- Made bootstrap directory preparation fail immediately if its private files + directory cannot be created instead of allowing a later generic write + failure. +- P2P bootstrap failures now prefer safely redacted Android `stderr` over the + generic `Command failed with exit code 1` status, retaining the actionable + cause without exposing the request ID or bootstrap secret. + +### Version + +- Updated OpenADB, ACBridge, Windows metadata, build workflows, screenshots, + and active release documentation to version 3.0.1. +- ACBridge 3.0.1 uses `versionCode 30101` under the documented version-code + policy and is rebuilt from source as `ACBridge-3.0.1.apk`. + +### Validation + +- Added regressions that require the nested `run-as ... sh -c` script to stay + in one `adb shell` argument and that preserve useful, redacted Android error + details when bootstrap creation fails. +- Passed all 39 isolated test modules and all 565 tests with + `ResourceWarning` treated as an error, plus compileall, Ruff, workflow YAML, + embedded PowerShell, immutable Action-reference, and diff validation. +- Rebuilt and independently verified both byte-identical 45,613-byte ACBridge + APK aliases: package/version metadata, ZIP alignment, v1/v2/v3 signatures, + and the established signer digest all pass. +- Regenerated and validated the seven versioned README screenshots. The + contextual Applications capture now uses the complete offscreen render path + and every persistent navigation row is checked. +- The failure was confirmed from a sanitized local device log, but no ADB + transport remained connected for a real post-fix phone transfer. No + hardware-transfer success is claimed by this changelog. + +## [3.0.0] — 2026-07-13 ### Version diff --git a/GUI_AUDIT.md b/GUI_AUDIT.md index 64d5d30..ade5cd3 100644 --- a/GUI_AUDIT.md +++ b/GUI_AUDIT.md @@ -2,7 +2,7 @@ Дата аудита: 10 июля 2026 года -Текущая версия: OpenADB 3.0.0 +Текущая версия: OpenADB 3.0.1 Среда: Windows, Python 3.14.3, PySide6 6.11.1, экран 1920×1080, системная тёмная тема diff --git a/GUI_REDESIGN_REPORT.md b/GUI_REDESIGN_REPORT.md index 207700a..40035a6 100644 --- a/GUI_REDESIGN_REPORT.md +++ b/GUI_REDESIGN_REPORT.md @@ -2,7 +2,7 @@ Дата: 12 июля 2026 года -Версия отчёта: OpenADB 3.0.0 +Версия отчёта: OpenADB 3.0.1 Платформа проверки: Windows, Python 3.14.3, PySide6 6.11.1 diff --git a/README.md b/README.md index 092629c..0a568ca 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![OpenADB logo](logo.png) -Version: `3.0.0` +Version: `3.0.1` OpenADB is a Windows desktop GUI for Android Platform Tools. It uses ADB and fastboot directly, without MTP and without root requirements, to inspect devices, manage apps, back up APKs before uninstalling, restore backups, transfer files, run common commands, and keep useful logs. @@ -14,35 +14,35 @@ The main window uses the same adaptive navigation, device status bar, keyboard f Dashboard keeps connection state and the recommended next action visible. Technical device information and Wireless ADB are compact, expandable sections. -![Dashboard in the dark theme](docs/screenshots/dashboard-dark-v3.0.0.png) +![Dashboard in the dark theme](docs/screenshots/dashboard-dark-v3.0.1.png) -![Dashboard in the light theme](docs/screenshots/dashboard-light-v3.0.0.png) +![Dashboard in the light theme](docs/screenshots/dashboard-light-v3.0.1.png) ### Applications Applications combines independent type, state, and UAD-category filters while preserving selections that are temporarily hidden by search or filtering. With no selection the table keeps its full height; selecting rows opens the contextual action bar inside the same table area. -![Applications with no selected rows](docs/screenshots/applications-dark-v3.0.0.png) +![Applications with no selected rows](docs/screenshots/applications-dark-v3.0.1.png) -![Applications contextual action bar](docs/screenshots/applications-contextual-actions-dark-v3.0.0.png) +![Applications contextual action bar](docs/screenshots/applications-contextual-actions-dark-v3.0.1.png) ### File Manager File Manager uses a resizable Android/action/Windows layout. Transfers, file operations, storage selection, optional existing-root support, and the Auto (recommended) or 1–8 manual stream selector for P2P uploads remain visible without hiding either file panel. -![File Manager in the dark theme](docs/screenshots/file-manager-dark-v3.0.0.png) +![File Manager in the dark theme](docs/screenshots/file-manager-dark-v3.0.1.png) ### Commands Commands provides a searchable Basic/Advanced catalog, availability and risk explanations, and an inline stdout/stderr result area. -![Commands in the dark theme](docs/screenshots/commands-dark-v3.0.0.png) +![Commands in the dark theme](docs/screenshots/commands-dark-v3.0.1.png) ### Settings Settings groups Platform Tools, appearance, device monitoring, application safety, root-assisted features, storage, and maintenance into scrollable sections. -![Settings in the dark theme](docs/screenshots/settings-dark-v3.0.0.png) +![Settings in the dark theme](docs/screenshots/settings-dark-v3.0.1.png) ## Independence and Attribution @@ -56,7 +56,7 @@ OpenADB uses its own package name for its optional Android bridge helper: com.communism420.acbridge ``` -The bundled `ACBridge-3.0.0.apk` is an independent helper built from the source in `openadb/resources/acbridge/`. Do not use ADB AppControl branding, package identity, code, or assets as OpenADB branding. +The bundled `ACBridge-3.0.1.apk` is an independent helper built from the source in `openadb/resources/acbridge/`. Do not use ADB AppControl branding, package identity, code, or assets as OpenADB branding. ## Acknowledgements @@ -214,9 +214,9 @@ Dashboard puts the textual connection state, active device, ADB/Recovery/Fastboo Apps lists installed packages with checkbox, icon or fallback icon, label/package name, type, state, version, APK paths, and size when Android allows it. -For faster real labels and rendered application icons, OpenADB automatically installs and starts its own helper APK, `com.communism420.acbridge`, from `openadb/resources/acbridge/ACBridge-3.0.0.apk`. The helper exports app labels and PNG icons through ADB-readable files, then OpenADB caches them locally. If the helper cannot be installed or started, OpenADB falls back to APK metadata parsing and clearly reports that fallback in the Apps status line. +For faster real labels and rendered application icons, OpenADB automatically installs and starts its own helper APK, `com.communism420.acbridge`, from `openadb/resources/acbridge/ACBridge-3.0.1.apk`. The helper exports app labels and PNG icons through ADB-readable files, then OpenADB caches them locally. If the helper cannot be installed or started, OpenADB falls back to APK metadata parsing and clearly reports that fallback in the Apps status line. -ACBridge 3.0.0 (`versionCode 30002`) exports only the packages OpenADB asks for, reports live label/icon progress, exports versionName/versionCode and APK size through Android PackageManager, stores pre-rendered PNG icons without extra ZIP recompression, and OpenADB imports those PNGs directly into the icon cache. Like ADB AppControl's bridge workflow, OpenADB exchanges compact cache files instead of pulling hundreds of APK files. On phones it keeps the public `/sdcard/.adac` exchange folder for compatibility; on Android TV it is packaged as a leanback-compatible helper and prefers its app-specific external folder first, because some TV firmwares restrict public hidden folders more aggressively. +ACBridge 3.0.1 (`versionCode 30101`) exports only the packages OpenADB asks for, reports live label/icon progress, exports versionName/versionCode and APK size through Android PackageManager, stores pre-rendered PNG icons without extra ZIP recompression, and OpenADB imports those PNGs directly into the icon cache. Like ADB AppControl's bridge workflow, OpenADB exchanges compact cache files instead of pulling hundreds of APK files. On phones it keeps the public `/sdcard/.adac` exchange folder for compatibility; on Android TV it is packaged as a leanback-compatible helper and prefers its app-specific external folder first, because some TV firmwares restrict public hidden folders more aggressively. OpenADB does not automatically delete an installed ACBridge package. If Android reports a signature mismatch while updating ACBridge, OpenADB keeps the existing helper and explains the issue. To move from an older manually built/debug-signed ACBridge to the bundled helper, uninstall `com.communism420.acbridge` manually and refresh Apps again. @@ -286,7 +286,7 @@ adb push ADB remains the default upload transport for a new device profile. For PC → Android uploads, the transport selector can instead use `P2P via ACBridge`. On the first unacknowledged P2P selection for each device profile, OpenADB explains that the connection is authenticated and file integrity is verified, but the file data is not encrypted. Accepting the warning suppresses repeats for the current run; selecting `Do not show this warning again` persists the acknowledgement only in that profile. Cancelling the warning keeps or restores ADB. While P2P is selected, the compact `Authenticated, not encrypted` status remains visible. Use P2P only on a trusted private network, never on public, shared, guest, or otherwise untrusted Wi-Fi. -P2P parallelism defaults to `Auto (recommended)`. Its deterministic planner selects 1–4 streams from the captured file count, total size, average size, and largest-file share. It does not probe, benchmark, or guess device or network speed. A per-profile manual override offers 1–8 streams; the actual count never exceeds the number of files, so a single file always uses one stream. OpenADB balances files between independent sessions by size and includes directory entries in those sessions; ACBridge serializes directory creation across concurrent sessions and keeps every individual file atomic. Platform Tools remains the control plane: OpenADB installs/updates the security-hardened ACBridge 3.0.0 build 2 (`versionCode 30002`), streams a one-shot authenticated request through ADB `run-as` standard input into ACBridge private app storage, starts short-lived foreground sessions, and retrieves their generated session keys. The request ID is only a public locator for request-scoped control files: the bootstrap secret stays inside the streamed payload, and the generated session key is returned only in authenticated `READY` metadata. On the first transfer to a MicroSD/USB location, ACBridge pauses in `PERMISSION_REQUIRED`, opens its Android storage-access flow, and waits for the user to approve the requested SAF tree or Android's `All files access` fallback. A P2P server does not open its TCP port and no file bytes are sent before that permission is available. File bytes then travel directly from the PC to the Android device over the local network. ACBridge writes them through the granted SAF tree, or through the granted storage-manager access on TV firmware without a working folder picker, so removable MicroSD/USB storage can be written without root even when the Android `shell` user is blocked. Android → PC transfers continue through Platform Tools in this version. +P2P parallelism defaults to `Auto (recommended)`. Its deterministic planner selects 1–4 streams from the captured file count, total size, average size, and largest-file share. It does not probe, benchmark, or guess device or network speed. A per-profile manual override offers 1–8 streams; the actual count never exceeds the number of files, so a single file always uses one stream. OpenADB balances files between independent sessions by size and includes directory entries in those sessions; ACBridge serializes directory creation across concurrent sessions and keeps every individual file atomic. Platform Tools remains the control plane: OpenADB installs/updates the security-hardened ACBridge 3.0.1 build 1 (`versionCode 30101`), streams a one-shot authenticated request through ADB `run-as` standard input into ACBridge private app storage, starts short-lived foreground sessions, and retrieves their generated session keys. The request ID is only a public locator for request-scoped control files: the bootstrap secret stays inside the streamed payload, and the generated session key is returned only in authenticated `READY` metadata. On the first transfer to a MicroSD/USB location, ACBridge pauses in `PERMISSION_REQUIRED`, opens its Android storage-access flow, and waits for the user to approve the requested SAF tree or Android's `All files access` fallback. A P2P server does not open its TCP port and no file bytes are sent before that permission is available. File bytes then travel directly from the PC to the Android device over the local network. ACBridge writes them through the granted SAF tree, or through the granted storage-manager access on TV firmware without a working folder picker, so removable MicroSD/USB storage can be written without root even when the Android `shell` user is blocked. Android → PC transfers continue through Platform Tools in this version. Each P2P session accepts one authenticated connection, and ACBridge can keep several selected sessions active concurrently. The foreground service stops only after every session has finished or timed out. Session keys are never placed in an ADB command line, and their authenticated `READY` status files are removed before data transfer. HMAC-SHA256 authenticates the connection, every entry-metadata control frame, the canonical request transcript, each file payload, and the terminal success response with exact entry/file/byte counts. SHA-256 verifies each completed file before ACBridge replaces an existing destination. Partial files use temporary SAF documents and are removed after cancellation or failure. These checks authenticate the one-shot session and verify integrity; they do not encrypt the file data. Use P2P only on a trusted private network. Router/AP client isolation and host firewalls can prevent the PC from reaching the TV directly. @@ -304,7 +304,7 @@ When `Use root for transfers` is explicitly enabled and root is already granted /mnt/media_rw/ ``` -File creation, deletion, rename, pull, and the default push transport work through ADB on the selected storage volume. If P2P upload or ACBridge deletion needs removable-storage access, OpenADB asks ACBridge to request Android Storage Access Framework access on the TV screen. Select the requested MicroSD/USB storage location once; Android persists that permission, and future P2P uploads and deletes can use `DocumentsContract` through ACBridge without MTP. ACBridge 3.0.0 opens the picker for the matching storage volume when Android exposes it, resolves files by traversing the granted SAF tree, and falls back to Android's All files access settings for OpenADB Bridge if the firmware has no system folder picker. If Android still denies write access, OpenADB reports the error instead of silently pretending the operation succeeded. +File creation, deletion, rename, pull, and the default push transport work through ADB on the selected storage volume. If P2P upload or ACBridge deletion needs removable-storage access, OpenADB asks ACBridge to request Android Storage Access Framework access on the TV screen. Select the requested MicroSD/USB storage location once; Android persists that permission, and future P2P uploads and deletes can use `DocumentsContract` through ACBridge without MTP. ACBridge 3.0.1 opens the picker for the matching storage volume when Android exposes it, resolves files by traversing the granted SAF tree, and falls back to Android's All files access settings for OpenADB Bridge if the firmware has no system folder picker. If Android still denies write access, OpenADB reports the error instead of silently pretending the operation succeeded. ## Commands diff --git a/docs/DEVICE_LAB_MATRIX.md b/docs/DEVICE_LAB_MATRIX.md index 0af1b89..a927403 100644 --- a/docs/DEVICE_LAB_MATRIX.md +++ b/docs/DEVICE_LAB_MATRIX.md @@ -1,4 +1,4 @@ -# OpenADB 3.0.0 device-lab matrix +# OpenADB 3.0.1 device-lab matrix Last updated: 2026-07-13 diff --git a/docs/RELEASE_PROCESS.md b/docs/RELEASE_PROCESS.md index 09b2fe7..6a9d4d0 100644 --- a/docs/RELEASE_PROCESS.md +++ b/docs/RELEASE_PROCESS.md @@ -1,14 +1,14 @@ # OpenADB release process This document is the operator checklist for producing an OpenADB Windows -release. It describes the automated 3.0.0 pipeline and the manual evidence +release. It describes the automated 3.0.1 pipeline and the manual evidence that must be retained. A green workflow is necessary, but it does not replace physical Windows or Android device-lab validation. ## Release invariants -- Build and release only from an immutable `v` tag. OpenADB 3.0.0 is - intentionally restricted to `v3.0.0` by the release workflow. +- Build and release only from an immutable `v` tag. OpenADB 3.0.1 is + intentionally restricted to `v3.0.1` by the release workflow. - The Python package, window title, Windows resources, PyInstaller filename, ACBridge source, manifest, APK, documentation, and changelog must name the same version. @@ -69,8 +69,8 @@ change and must pass the complete Windows Python matrix before release. python -m unittest -q tests.test_version_metadata ``` -For OpenADB 3.0.0 the required helper identity is -`com.communism420.acbridge`, `versionName=3.0.0`, and `versionCode=30002`. +For OpenADB 3.0.1 the required helper identity is +`com.communism420.acbridge`, `versionName=3.0.1`, and `versionCode=30101`. ## 2. Build and verify ACBridge @@ -94,9 +94,9 @@ Record the output of the following independent checks in the release evidence: ```powershell $buildTools = Get-ChildItem "$env:ANDROID_HOME\build-tools" -Directory | Sort-Object Name -Descending | Select-Object -First 1 -& "$($buildTools.FullName)\aapt.exe" dump badging openadb\resources\acbridge\ACBridge-3.0.0.apk -& "$($buildTools.FullName)\zipalign.exe" -c -v 4 openadb\resources\acbridge\ACBridge-3.0.0.apk -java -jar "$($buildTools.FullName)\lib\apksigner.jar" verify --verbose --print-certs openadb\resources\acbridge\ACBridge-3.0.0.apk +& "$($buildTools.FullName)\aapt.exe" dump badging openadb\resources\acbridge\ACBridge-3.0.1.apk +& "$($buildTools.FullName)\zipalign.exe" -c -v 4 openadb\resources\acbridge\ACBridge-3.0.1.apk +java -jar "$($buildTools.FullName)\lib\apksigner.jar" verify --verbose --print-certs openadb\resources\acbridge\ACBridge-3.0.1.apk ``` The bundled ACBridge APK uses the repository's intentionally public Android @@ -156,12 +156,12 @@ python -m pip check python -m PyInstaller --noconfirm --clean OpenADB.spec ``` -`OpenADB.spec` produces a one-file `OpenADB-3.0.0.exe` build intermediate and +`OpenADB.spec` produces a one-file `OpenADB-3.0.1.exe` build intermediate and bundles the current ADB/fastboot binaries and DLLs, their notice when available, the versioned ACBridge APK, UI resources, and required Python packages. Until Authenticode succeeds, that stable-looking intermediate is not a publishable stable artifact: inspect it, then rename it to -`OpenADB-3.0.0-unsigned.exe`. Do not commit the large EXE; publish it as an +`OpenADB-3.0.1-unsigned.exe`. Do not commit the large EXE; publish it as an Actions/release artifact. Automation downloads the exact stable Platform Tools 37.0.0 Windows archive. @@ -170,17 +170,17 @@ recorded SHA-256 of those same bytes before extracting or executing anything; either mismatch stops the build. The `Windows release build` workflow runs on `workflow_dispatch`, calls from -the release workflow, and the exact `v3.0.0` tag. Its smoke test uses a clean +the release workflow, and the exact `v3.0.1` tag. Its smoke test uses a clean temporary OpenADB profile and a read-only startup path. It checks: - process startup and clean shutdown; -- the exact `OpenADB 3.0.0` window title; +- the exact `OpenADB 3.0.1` window title; - bundled ADB, fastboot, Platform Tools libraries, and notice; - bundled ACBridge package/version metadata; - absence of a crash log. -The workflow uploads either `OpenADB-3.0.0-windows-signed` or -`OpenADB-3.0.0-windows-unsigned`. The artifact contains exactly one +The workflow uploads either `OpenADB-3.0.1-windows-signed` or +`OpenADB-3.0.1-windows-unsigned`. The artifact contains exactly one appropriately named EXE plus `BUILD_STATUS.json` and `SHA256SUMS.txt`; its dynamic artifact name, signed state, and filename are also exposed as reusable workflow outputs and must agree with the status file. Treat a missing or @@ -216,10 +216,10 @@ configuration is an error. When all are present, automation must: 1. decode the PFX into the isolated runner temporary directory; 2. use it without echoing the password or certificate bytes; -3. sign the temporary `OpenADB-3.0.0-unsigned.exe` candidate with SHA-256 and +3. sign the temporary `OpenADB-3.0.1-unsigned.exe` candidate with SHA-256 and the configured timestamp; 4. run `signtool verify /pa /all /v /tw` and require exit code zero; -5. only after verification, rename it to the stable `OpenADB-3.0.0.exe`; +5. only after verification, rename it to the stable `OpenADB-3.0.1.exe`; 6. independently check Authenticode again in the release job; 7. delete the temporary PFX in an always-run cleanup step (and delete any temporary certificate-store entry if a future implementation imports one). @@ -241,7 +241,7 @@ Tools archive hashes so the release gate can reject a substituted build input. Local verification uses: ```powershell -$executables = @(Get-ChildItem . -File -Filter 'OpenADB-3.0.0*.exe') +$executables = @(Get-ChildItem . -File -Filter 'OpenADB-3.0.1*.exe') if ($executables.Count -ne 1) { throw 'Expected exactly one signed or unsigned release EXE.' } $digest = Get-FileHash $executables[0].FullName -Algorithm SHA256 "$($digest.Hash) *$($executables[0].Name)" | Set-Content .\SHA256SUMS.txt -Encoding ascii @@ -252,8 +252,8 @@ For a verified signed build, first require the stable filename and then retain this command's successful output: ```powershell -if ($executables[0].Name -ne 'OpenADB-3.0.0.exe') { throw 'Unsigned EXE cannot pass the signed gate.' } -signtool verify /pa /all /v /tw .\OpenADB-3.0.0.exe +if ($executables[0].Name -ne 'OpenADB-3.0.1.exe') { throw 'Unsigned EXE cannot pass the signed gate.' } +signtool verify /pa /all /v /tw .\OpenADB-3.0.1.exe ``` Do not copy a checksum from an earlier build: signing changes the executable @@ -281,9 +281,9 @@ commit and push only that tag: ```powershell git status --short -git tag -a v3.0.0 -m "OpenADB 3.0.0" -git show --no-patch --decorate v3.0.0 -git push origin v3.0.0 +git tag -a v3.0.1 -m "OpenADB 3.0.1" +git show --no-patch --decorate v3.0.1 +git push origin v3.0.1 ``` The tag starts `Windows CI`, the standalone Windows build, and the release @@ -292,7 +292,7 @@ same reusable Windows builder, downloads its artifact, validates the strict metadata schema, recomputes hashes, independently verifies Authenticode, and only then calls GitHub Releases. -Release notes are generated from the English 3.0.0 changelog section and add: +Release notes are generated from the English 3.0.1 changelog section and add: - signed/unsigned state and executable SHA-256; - Platform Tools and ACBridge metadata; @@ -307,7 +307,7 @@ profiles, signing password, and successful-test logs are never release assets. ## 9. Behavior without a signing certificate With all three signing secrets absent, the builder creates -`OpenADB-3.0.0-unsigned.exe`, records `"signed": false`, and never uses the +`OpenADB-3.0.1-unsigned.exe`, records `"signed": false`, and never uses the stable signed filename. An automatic tag run creates only a clearly labelled draft/prerelease unsigned preview. Reviewers must check its checksum, metadata, limitations, and Windows warning behavior before deciding what to do next. @@ -315,13 +315,13 @@ limitations, and Windows warning behavior before deciding what to do next. The preferred resolution is to configure a protected certificate and rerun the pipeline. If project policy explicitly permits an unsigned stable release, a maintainer must delete the existing draft preview after preserving its audit -record, select the `v3.0.0` ref in Actions, manually dispatch -`OpenADB 3.0.0 release`, and enable `allow_unsigned_stable`. That explicit +record, select the `v3.0.1` ref in Actions, manually dispatch +`OpenADB 3.0.1 release`, and enable `allow_unsigned_stable`. That explicit input is unavailable to an automatic tag run. The published executable still keeps the `-unsigned` suffix and the release notes prominently disclose its state. -Never rename an unsigned EXE to `OpenADB-3.0.0.exe`, manually set +Never rename an unsigned EXE to `OpenADB-3.0.1.exe`, manually set `"signed": true`, or publish an unsigned automatic preview as a final signed release. diff --git a/docs/screenshots/applications-contextual-actions-dark-v3.0.0.png b/docs/screenshots/applications-contextual-actions-dark-v3.0.0.png deleted file mode 100644 index 26475b8..0000000 Binary files a/docs/screenshots/applications-contextual-actions-dark-v3.0.0.png and /dev/null differ diff --git a/docs/screenshots/applications-contextual-actions-dark-v3.0.1.png b/docs/screenshots/applications-contextual-actions-dark-v3.0.1.png new file mode 100644 index 0000000..e614da3 Binary files /dev/null and b/docs/screenshots/applications-contextual-actions-dark-v3.0.1.png differ diff --git a/docs/screenshots/applications-dark-v3.0.0.png b/docs/screenshots/applications-dark-v3.0.0.png deleted file mode 100644 index a70c724..0000000 Binary files a/docs/screenshots/applications-dark-v3.0.0.png and /dev/null differ diff --git a/docs/screenshots/applications-dark-v3.0.1.png b/docs/screenshots/applications-dark-v3.0.1.png new file mode 100644 index 0000000..5179bfd Binary files /dev/null and b/docs/screenshots/applications-dark-v3.0.1.png differ diff --git a/docs/screenshots/commands-dark-v3.0.0.png b/docs/screenshots/commands-dark-v3.0.0.png deleted file mode 100644 index c38cc64..0000000 Binary files a/docs/screenshots/commands-dark-v3.0.0.png and /dev/null differ diff --git a/docs/screenshots/commands-dark-v3.0.1.png b/docs/screenshots/commands-dark-v3.0.1.png new file mode 100644 index 0000000..f8a6806 Binary files /dev/null and b/docs/screenshots/commands-dark-v3.0.1.png differ diff --git a/docs/screenshots/dashboard-dark-v3.0.0.png b/docs/screenshots/dashboard-dark-v3.0.0.png deleted file mode 100644 index 12246d6..0000000 Binary files a/docs/screenshots/dashboard-dark-v3.0.0.png and /dev/null differ diff --git a/docs/screenshots/dashboard-dark-v3.0.1.png b/docs/screenshots/dashboard-dark-v3.0.1.png new file mode 100644 index 0000000..a520f2f Binary files /dev/null and b/docs/screenshots/dashboard-dark-v3.0.1.png differ diff --git a/docs/screenshots/dashboard-light-v3.0.0.png b/docs/screenshots/dashboard-light-v3.0.0.png deleted file mode 100644 index c05ecc4..0000000 Binary files a/docs/screenshots/dashboard-light-v3.0.0.png and /dev/null differ diff --git a/docs/screenshots/dashboard-light-v3.0.1.png b/docs/screenshots/dashboard-light-v3.0.1.png new file mode 100644 index 0000000..a52052e Binary files /dev/null and b/docs/screenshots/dashboard-light-v3.0.1.png differ diff --git a/docs/screenshots/file-manager-dark-v3.0.0.png b/docs/screenshots/file-manager-dark-v3.0.0.png deleted file mode 100644 index 76e9172..0000000 Binary files a/docs/screenshots/file-manager-dark-v3.0.0.png and /dev/null differ diff --git a/docs/screenshots/file-manager-dark-v3.0.1.png b/docs/screenshots/file-manager-dark-v3.0.1.png new file mode 100644 index 0000000..f1782b0 Binary files /dev/null and b/docs/screenshots/file-manager-dark-v3.0.1.png differ diff --git a/docs/screenshots/settings-dark-v3.0.0.png b/docs/screenshots/settings-dark-v3.0.0.png deleted file mode 100644 index aa43a68..0000000 Binary files a/docs/screenshots/settings-dark-v3.0.0.png and /dev/null differ diff --git a/docs/screenshots/settings-dark-v3.0.1.png b/docs/screenshots/settings-dark-v3.0.1.png new file mode 100644 index 0000000..7b733ee Binary files /dev/null and b/docs/screenshots/settings-dark-v3.0.1.png differ diff --git a/openadb/core/acbridge_p2p.py b/openadb/core/acbridge_p2p.py index c6662b8..3ec34d0 100644 --- a/openadb/core/acbridge_p2p.py +++ b/openadb/core/acbridge_p2p.py @@ -645,7 +645,7 @@ def _prepare_session_locked( remote_bootstrap_started = True with self._private_adb_log("prepare request", request_id): prepare_command = ( - "mkdir -p files; " + "mkdir -p files && " f"rm -f {shell_quote(remote_request)} {shell_quote(remote_cancel)}" ) prepared = self.adb.run_shell( @@ -656,9 +656,9 @@ def _prepare_session_locked( ) self._check_cancelled(cancel_event) if not prepared.success: + detail = prepared.stderr or prepared.status raise P2PTransferError( - prepared.status - or prepared.stderr + _redact_exact(detail, request_id, bootstrap_secret) or "Could not prepare the ACBridge P2P request folder." ) self._remove_status_file(request_id, cancel_event=cancel_event) @@ -668,6 +668,17 @@ def write_request(stream: BinaryIO) -> None: stream.write(request_text.encode("utf-8")) stream.flush() + # `adb shell` reconstructs multiple argv items as one remote shell + # command. Passing run-as/sh/-c/redirection as separate items lets + # Android's outer shell consume `>` before run-as is entered. Keep + # the complete nested command in the single argument after + # `shell`, so the request is written inside ACBridge's private + # files directory rather than the shell user's working directory. + write_script = f"cat > {shell_quote(remote_request)}" + remote_write_command = ( + f"run-as {shell_quote(ACBridgeClient.PACKAGE)} " + f"sh -c {shell_quote(write_script)}" + ) try: with self._private_adb_log( "write request", @@ -675,28 +686,21 @@ def write_request(stream: BinaryIO) -> None: bootstrap_secret, ): pushed = self.adb.run_raw_with_input_stream( - [ - "shell", - "run-as", - ACBridgeClient.PACKAGE, - "sh", - "-c", - f"cat > {shell_quote(remote_request)}", - ], + ["shell", remote_write_command], input_writer=write_request, timeout=30, cancel_event=cancel_event, ) except Exception as exc: - detail = _redact_exact(str(exc), bootstrap_secret) + detail = _redact_exact(str(exc), request_id, bootstrap_secret) raise P2PTransferError( detail or "Could not pass the P2P request to ACBridge." ) from None self._check_cancelled(cancel_event) if not pushed.success: - detail = pushed.status or pushed.stderr + detail = pushed.stderr or pushed.status raise P2PTransferError( - _redact_exact(detail, bootstrap_secret) + _redact_exact(detail, request_id, bootstrap_secret) or "Could not pass the P2P request to ACBridge." ) self._check_cancelled(cancel_event) @@ -711,9 +715,9 @@ def write_request(stream: BinaryIO) -> None: service_started = bool(started.success) self._check_cancelled(cancel_event) if not started.success: + detail = started.stderr or started.status raise P2PTransferError( - started.status - or started.stderr + _redact_exact(detail, request_id, bootstrap_secret) or "Android refused to start the ACBridge P2P foreground service." ) except Exception: diff --git a/openadb/resources/acbridge/ACBridge-3.0.0.apk b/openadb/resources/acbridge/ACBridge-3.0.0.apk deleted file mode 100644 index 10d6995..0000000 Binary files a/openadb/resources/acbridge/ACBridge-3.0.0.apk and /dev/null differ diff --git a/openadb/resources/acbridge/ACBridge-3.0.1.apk b/openadb/resources/acbridge/ACBridge-3.0.1.apk new file mode 100644 index 0000000..d958cbd Binary files /dev/null and b/openadb/resources/acbridge/ACBridge-3.0.1.apk differ diff --git a/openadb/resources/acbridge/ACBridge.apk b/openadb/resources/acbridge/ACBridge.apk index 10d6995..d958cbd 100644 Binary files a/openadb/resources/acbridge/ACBridge.apk and b/openadb/resources/acbridge/ACBridge.apk differ diff --git a/openadb/resources/acbridge/AndroidManifest.xml b/openadb/resources/acbridge/AndroidManifest.xml index 1284256..88f1240 100644 --- a/openadb/resources/acbridge/AndroidManifest.xml +++ b/openadb/resources/acbridge/AndroidManifest.xml @@ -1,7 +1,7 @@ + android:versionCode="30101" + android:versionName="3.0.1"> diff --git a/openadb/resources/acbridge/src/com/communism420/acbridge/BuildInfo.java b/openadb/resources/acbridge/src/com/communism420/acbridge/BuildInfo.java index 8cc7eb0..87b6336 100644 --- a/openadb/resources/acbridge/src/com/communism420/acbridge/BuildInfo.java +++ b/openadb/resources/acbridge/src/com/communism420/acbridge/BuildInfo.java @@ -2,8 +2,8 @@ /** Release metadata mirrored by the manifest and verified by the desktop test suite. */ final class BuildInfo { - static final String VERSION_NAME = "3.0.0"; - static final long VERSION_CODE = 30002L; + static final String VERSION_NAME = "3.0.1"; + static final long VERSION_CODE = 30101L; private BuildInfo() { } diff --git a/openadb/version.py b/openadb/version.py index c202217..885aa7d 100644 --- a/openadb/version.py +++ b/openadb/version.py @@ -3,15 +3,15 @@ from __future__ import annotations -VERSION = "3.0.0" -VERSION_PARTS = (3, 0, 0) +VERSION = "3.0.1" +VERSION_PARTS = (3, 0, 1) # Android versionCode policy: major * 10_000 + minor * 1_000 + patch * 100 -# + build. This preserves the established sequence 20004 (2.0.0 build 4) and -# 20101 (2.0.1 build 1). Build 2 carries the fixed, nonsecret P2P bootstrap -# protocol introduced during the 3.0.0 hardening cycle. -ACBRIDGE_BUILD = 2 -ACBRIDGE_VERSION_CODE = 30002 +# + build. This preserves the established sequence 20004 (2.0.0 build 4), +# 20101 (2.0.1 build 1), and 30002 (3.0.0 build 2). Patch release 3.0.1 starts +# its helper build sequence at 1 and therefore uses versionCode 30101. +ACBRIDGE_BUILD = 1 +ACBRIDGE_VERSION_CODE = 30101 ACBRIDGE_PACKAGE = "com.communism420.acbridge" ACBRIDGE_APK_FILENAME = f"ACBridge-{VERSION}.apk" ACBRIDGE_SIGNER_SHA256 = "57d0f9154b24fa9e5aebf40e4e4b8f83c42b281e08e22d4cc34ee842c030ecd7" diff --git a/tests/test_acbridge_p2p.py b/tests/test_acbridge_p2p.py index 63d589c..b18e115 100644 --- a/tests/test_acbridge_p2p.py +++ b/tests/test_acbridge_p2p.py @@ -2,13 +2,14 @@ import hashlib import hmac +import shlex import socket import struct import tempfile import threading import time import unittest -from contextlib import contextmanager +from contextlib import contextmanager, nullcontext from io import BytesIO from pathlib import Path from types import SimpleNamespace @@ -496,6 +497,7 @@ def test_adb_bootstrap_never_places_session_key_on_command_line(self) -> None: bootstrap_secret = "b2" * 32 commands: list[str] = [] pushed_text: list[str] = [] + stream_args: list[list[str]] = [] granted_paths: list[str] = [] status_reads = 0 @@ -547,6 +549,7 @@ def run_raw_with_input_stream( ): stream = BytesIO() input_writer(stream) + stream_args.append(list(args)) record_private_command(" ".join(args)) pushed_text.append(stream.getvalue().decode("utf-8")) return SimpleNamespace( @@ -620,6 +623,20 @@ def grant_storage_access(path: str, timeout: int, cancel_event=None): self.assertEqual(session.port, 42042) self.assertEqual(session.token, "c" * 64) + self.assertEqual(len(stream_args), 1) + self.assertEqual(stream_args[0][0], "shell") + self.assertEqual(len(stream_args[0]), 2) + self.assertEqual( + shlex.split(stream_args[0][1]), + [ + "run-as", + ACBridgeClient.PACKAGE, + "sh", + "-c", + f"cat > 'files/p2p_request_{request_id}.txt'", + ], + ) + self.assertNotIn(bootstrap_secret, stream_args[0][1]) self.assertTrue(pushed_text[0].startswith("OPENADB_P2P_2\n0\n")) self.assertIn(bootstrap_secret, pushed_text[0]) self.assertNotIn("c" * 64, "\n".join(commands)) @@ -732,6 +749,63 @@ def run_raw_with_input_stream(_args, **_kwargs): self.assertIn("[private]", str(raised.exception)) self.assertTrue(any(bootstrap_secret in values for values in scopes)) + def test_bootstrap_write_failure_prefers_actionable_stderr(self) -> None: + request_id = "d5" * 16 + bootstrap_secret = "e6" * 32 + + class FakeAdb: + runner = SimpleNamespace(scoped_log_command=lambda *_args, **_kwargs: nullcontext()) + + @staticmethod + def device_ip_addresses(cancel_event=None): + return ["192.0.2.10"] + + @staticmethod + def run_shell(_command, **_kwargs): + return SimpleNamespace(success=True, stdout="", stderr="", status="") + + @staticmethod + def run_raw_with_input_stream(_args, **_kwargs): + return SimpleNamespace( + success=False, + stdout="", + stderr=( + "/system/bin/sh: can't create " + f"files/p2p_request_{request_id}.txt: No such file or directory" + ), + status="Command failed with exit code 1", + ) + + bridge = SimpleNamespace( + adb=FakeAdb(), + settings=SimpleNamespace(temp_folder=Path("unused")), + ensure_installed=lambda **_kwargs: (True, "ready"), + ) + client = ACBridgeP2PClient(bridge) + with ( + patch( + "openadb.core.acbridge_p2p.uuid.uuid4", + return_value=SimpleNamespace(hex=request_id), + ), + patch( + "openadb.core.acbridge_p2p.secrets.token_hex", + return_value=bootstrap_secret, + ), + self.assertRaises(P2PTransferError) as raised, + ): + client._prepare_session( + "/storage/emulated/0/Download", + timeout_seconds=120, + connect_timeout=2, + ) + + message = str(raised.exception) + self.assertIn("can't create files/p2p_request_[private].txt", message) + self.assertIn("No such file or directory", message) + self.assertNotIn("Command failed with exit code 1", message) + self.assertNotIn(request_id, message) + self.assertNotIn(bootstrap_secret, message) + def test_collects_files_and_empty_directories_without_flattening(self) -> None: with tempfile.TemporaryDirectory() as temp: root = Path(temp) / "Media" diff --git a/tests/test_device_lab_smoke.py b/tests/test_device_lab_smoke.py index 90b0d29..8a56c36 100644 --- a/tests/test_device_lab_smoke.py +++ b/tests/test_device_lab_smoke.py @@ -165,7 +165,7 @@ def test_no_hardware_is_truthfully_not_run_in_json_and_junit(self) -> None: temporary_reports = tuple(Path(temp).glob(".*.tmp")) self.assertEqual(payload["schema"], REPORT_SCHEMA) - self.assertEqual(payload["product"]["version"], "3.0.0") + self.assertEqual(payload["product"]["version"], "3.0.1") self.assertRegex(payload["product"]["source_commit"], r"^(?:unavailable|[0-9a-f]{40})$") self.assertEqual(set(payload["environment"]), {"os", "release", "architecture"}) self.assertEqual(payload["status"], "not_run") diff --git a/tests/test_version_metadata.py b/tests/test_version_metadata.py index 8bba84e..bc1fe80 100644 --- a/tests/test_version_metadata.py +++ b/tests/test_version_metadata.py @@ -29,35 +29,36 @@ ANDROID_NS = "{http://schemas.android.com/apk/res/android}" BRIDGE_ROOT = ROOT / "openadb" / "resources" / "acbridge" EXPECTED_SCREENSHOTS = { - "applications-contextual-actions-dark-v3.0.0.png", - "applications-dark-v3.0.0.png", - "commands-dark-v3.0.0.png", - "dashboard-dark-v3.0.0.png", - "dashboard-light-v3.0.0.png", - "file-manager-dark-v3.0.0.png", - "settings-dark-v3.0.0.png", + f"applications-contextual-actions-dark-v{VERSION}.png", + f"applications-dark-v{VERSION}.png", + f"commands-dark-v{VERSION}.png", + f"dashboard-dark-v{VERSION}.png", + f"dashboard-light-v{VERSION}.png", + f"file-manager-dark-v{VERSION}.png", + f"settings-dark-v{VERSION}.png", } class VersionMetadataTests(unittest.TestCase): def test_openadb_public_version_is_consistent(self) -> None: - self.assertEqual(VERSION, "3.0.0") + self.assertEqual(VERSION, "3.0.1") self.assertEqual(__version__, VERSION) - self.assertEqual(RELEASE_EXE_FILENAME, "OpenADB-3.0.0.exe") - self.assertIn("Version: `3.0.0`", (ROOT / "README.md").read_text(encoding="utf-8")) - self.assertIn("## [3.0.0]", (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")) - self.assertIn("## [3.0.0]", (ROOT / "CHANGELOG_EN.md").read_text(encoding="utf-8")) - self.assertIn("OpenADB 3.0.0", (ROOT / "GUI_AUDIT.md").read_text(encoding="utf-8")) - self.assertIn("OpenADB 3.0.0", (ROOT / "GUI_REDESIGN_REPORT.md").read_text(encoding="utf-8")) + self.assertEqual(RELEASE_EXE_FILENAME, "OpenADB-3.0.1.exe") + self.assertIn("Version: `3.0.1`", (ROOT / "README.md").read_text(encoding="utf-8")) + self.assertIn("## [3.0.1]", (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")) + self.assertIn("## [3.0.1]", (ROOT / "CHANGELOG_EN.md").read_text(encoding="utf-8")) + self.assertIn("OpenADB 3.0.1", (ROOT / "GUI_AUDIT.md").read_text(encoding="utf-8")) + self.assertIn("OpenADB 3.0.1", (ROOT / "GUI_REDESIGN_REPORT.md").read_text(encoding="utf-8")) def test_android_version_code_policy_is_documented_and_monotonic(self) -> None: - self.assertEqual(VERSION_PARTS, (3, 0, 0)) - self.assertEqual(ACBRIDGE_BUILD, 2) + self.assertEqual(VERSION_PARTS, (3, 0, 1)) + self.assertEqual(ACBRIDGE_BUILD, 1) self.assertEqual(android_version_code((2, 0, 0), 4), 20004) self.assertEqual(android_version_code((2, 0, 1), 1), 20101) - self.assertEqual(android_version_code(VERSION_PARTS, ACBRIDGE_BUILD), 30002) - self.assertEqual(ACBRIDGE_VERSION_CODE, 30002) - self.assertGreater(ACBRIDGE_VERSION_CODE, 20101) + self.assertEqual(android_version_code((3, 0, 0), 2), 30002) + self.assertEqual(android_version_code(VERSION_PARTS, ACBRIDGE_BUILD), 30101) + self.assertEqual(ACBRIDGE_VERSION_CODE, 30101) + self.assertGreater(ACBRIDGE_VERSION_CODE, 30002) self.assertRegex(ACBRIDGE_SIGNER_SHA256, r"^[0-9a-f]{64}$") def test_acbridge_source_metadata_matches_client(self) -> None: @@ -107,11 +108,18 @@ def test_pyinstaller_and_windows_metadata_match_release_artifact(self) -> None: self.assertNotIn("ACBridge-2.0.1.apk", spec) windows_metadata = (ROOT / "tools" / "openadb_version_info.txt").read_text(encoding="utf-8") - self.assertIn("filevers=(3, 0, 0, 0)", windows_metadata) - self.assertIn("prodvers=(3, 0, 0, 0)", windows_metadata) - self.assertIn("FileVersion', '3.0.0'", windows_metadata) + self.assertIn("filevers=(3, 0, 1, 0)", windows_metadata) + self.assertIn("prodvers=(3, 0, 1, 0)", windows_metadata) + self.assertIn("FileVersion', '3.0.1'", windows_metadata) self.assertIn(f"OriginalFilename', '{RELEASE_EXE_FILENAME}'", windows_metadata) - self.assertIn("ProductVersion', '3.0.0'", windows_metadata) + self.assertIn("ProductVersion', '3.0.1'", windows_metadata) + + release_workflow = (ROOT / ".github" / "workflows" / "release.yml").read_text( + encoding="utf-8" + ) + escaped_version = re.escape(VERSION) + self.assertIn(rf"^## \[{escaped_version}\]", release_workflow) + self.assertNotIn(r"^## \[3\.0\.0\]", release_workflow) def test_release_screenshot_names_match_version(self) -> None: screenshots = ROOT / "docs" / "screenshots" @@ -121,7 +129,8 @@ def test_release_screenshot_names_match_version(self) -> None: for filename in EXPECTED_SCREENSHOTS: screenshot = screenshots / filename self.assertIn(f"docs/screenshots/{filename}", readme) - self.assertIn(filename, generator) + generated_pattern = filename.replace(VERSION, "{VERSION}") + self.assertIn(generated_pattern, generator) self.assertLess(screenshot.stat().st_size, 1_000_000) with Image.open(screenshot) as image: image.load() diff --git a/tools/capture_readme_screenshots.py b/tools/capture_readme_screenshots.py index 9555dd2..20e9812 100644 --- a/tools/capture_readme_screenshots.py +++ b/tools/capture_readme_screenshots.py @@ -39,6 +39,7 @@ from openadb.ui.main_window import MainWindow from openadb.ui.style import apply_theme from openadb.ui.widgets.file_panel import FilePanel +from openadb.version import VERSION OUTPUT_DIR = ROOT / "docs" / "screenshots" @@ -341,7 +342,7 @@ def _capture(window: MainWindow, app: QApplication, page_name: str, filename: st # Give layouts and item views enough time to settle after configuring the # stacked page and any contextual action bar. contextual_apps = ( - filename == "applications-contextual-actions-dark-v3.0.0.png" + filename == f"applications-contextual-actions-dark-v{VERSION}.png" and app.platformName().casefold() == "offscreen" ) for _ in range(6 if contextual_apps else 3): @@ -463,7 +464,7 @@ def _capture_fresh_page( _configure_demo(window, demo_windows_dir, tools) if selected_packages is not None: _set_demo_app_selection(window, selected_packages) - if window.windowTitle() != "OpenADB 3.0.0": + if window.windowTitle() != f"OpenADB {VERSION}": raise RuntimeError(f"Unexpected screenshot title: {window.windowTitle()!r}") _capture(window, app, page_name, filename) print(f"Captured {filename}") @@ -478,18 +479,18 @@ def _capture_fresh_page( CAPTURE_TARGETS: dict[str, tuple[str, str, str, set[str] | None]] = { - "dashboard-dark": ("Dashboard", "Dark", "dashboard-dark-v3.0.0.png", None), - "dashboard-light": ("Dashboard", "Light", "dashboard-light-v3.0.0.png", None), - "applications": ("Apps", "Dark", "applications-dark-v3.0.0.png", set()), + "dashboard-dark": ("Dashboard", "Dark", f"dashboard-dark-v{VERSION}.png", None), + "dashboard-light": ("Dashboard", "Light", f"dashboard-light-v{VERSION}.png", None), + "applications": ("Apps", "Dark", f"applications-dark-v{VERSION}.png", set()), "applications-contextual": ( "Apps", "Dark", - "applications-contextual-actions-dark-v3.0.0.png", + f"applications-contextual-actions-dark-v{VERSION}.png", {"com.example.camera", "com.example.notes"}, ), - "file-manager": ("File Manager", "Dark", "file-manager-dark-v3.0.0.png", None), - "commands": ("Commands", "Dark", "commands-dark-v3.0.0.png", None), - "settings": ("Settings", "Dark", "settings-dark-v3.0.0.png", None), + "file-manager": ("File Manager", "Dark", f"file-manager-dark-v{VERSION}.png", None), + "commands": ("Commands", "Dark", f"commands-dark-v{VERSION}.png", None), + "settings": ("Settings", "Dark", f"settings-dark-v{VERSION}.png", None), } @@ -560,6 +561,13 @@ def _validate_captured_frame(filename: str) -> None: bright_pixels = sum(region.histogram()[131:]) if bright_pixels < minimum: raise RuntimeError(f"Incomplete Windows frame: {filename}") + # Total foreground counts can still pass when a native child keeps + # only two navigation rows and drops the others. Require visible icon + # or label pixels in every persistent navigation slot. + for top in (130, 175, 220, 265, 310, 355, 400): + navigation_row = grayscale.crop((20, top, 220, top + 45)) + if sum(navigation_row.histogram()[131:]) < 100: + raise RuntimeError(f"Incomplete navigation frame: {filename}") def main() -> int: @@ -568,12 +576,19 @@ def main() -> int: script = str(Path(__file__).resolve()) for target_name in CAPTURE_TARGETS: filename = CAPTURE_TARGETS[target_name][2] + capture_environment = os.environ.copy() + if target_name == "applications-contextual": + # The dynamic action bar can invalidate only part of the + # native Windows backing store. The explicit offscreen render + # path paints every visible widget into one complete frame. + capture_environment["QT_QPA_PLATFORM"] = "offscreen" for attempt in range(1, 4): subprocess.run( [sys.executable, script, "--capture", target_name], cwd=ROOT, check=True, timeout=60, + env=capture_environment, ) try: _validate_captured_frame(filename) diff --git a/tools/openadb_version_info.txt b/tools/openadb_version_info.txt index d98930c..3ffe1be 100644 --- a/tools/openadb_version_info.txt +++ b/tools/openadb_version_info.txt @@ -1,7 +1,7 @@ VSVersionInfo( ffi=FixedFileInfo( - filevers=(3, 0, 0, 0), - prodvers=(3, 0, 0, 0), + filevers=(3, 0, 1, 0), + prodvers=(3, 0, 1, 0), mask=0x3f, flags=0x0, OS=0x40004, @@ -16,12 +16,12 @@ VSVersionInfo( [ StringStruct('CompanyName', 'OpenADB'), StringStruct('FileDescription', 'OpenADB — Android Platform Tools GUI'), - StringStruct('FileVersion', '3.0.0'), + StringStruct('FileVersion', '3.0.1'), StringStruct('InternalName', 'OpenADB'), StringStruct('LegalCopyright', 'OpenADB contributors'), - StringStruct('OriginalFilename', 'OpenADB-3.0.0.exe'), + StringStruct('OriginalFilename', 'OpenADB-3.0.1.exe'), StringStruct('ProductName', 'OpenADB'), - StringStruct('ProductVersion', '3.0.0') + StringStruct('ProductVersion', '3.0.1') ] ) ]),