/
DemienMedich
/
BodyweightBase
Обзор
Документация
Войти
/
DemienMedich
/
BodyweightBase
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
verify-android.ps1
218 строк
10 KB
DemienMedich
feat: add profile weekly goals
03 авг 2026, 01:15
03 авг 2026, 01:15
a8f4bdb
Код
Авторство
О чём код?
param( [string]$ApkPath = (Join-Path $PSScriptRoot "dist\Android\BodyweightBaseQt-arm64.apk"), [string]$AdbPath = "Z:\Android\Sdk\platform-tools\adb.exe", [string]$AaptPath = "Z:\Android\Sdk\build-tools\35.0.1\aapt.exe", [string]$PackageName = "org.qtproject.example.BodyweightBaseCppApp", [string]$DeviceId = "", [switch]$Install ) $ErrorActionPreference = "Stop" function Get-TextSha256([string]$Text) { $bytes = [System.Text.Encoding]::UTF8.GetBytes($Text) return [Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData($bytes)) } if (-not (Test-Path -LiteralPath $ApkPath)) { throw "APK was not found: $ApkPath" } if (-not (Test-Path -LiteralPath $AaptPath)) { throw "aapt was not found: $AaptPath" } $workoutFeedbackPath = Join-Path $PSScriptRoot "app\android\src\org\bodyweightbase\android\WorkoutFeedback.kt" if (-not (Test-Path -LiteralPath $workoutFeedbackPath)) { throw "Android workout feedback bridge was not found: $workoutFeedbackPath" } $workoutFeedbackSource = Get-Content -LiteralPath $workoutFeedbackPath -Raw $requiredFeedbackPatterns = [ordered]@{ "WorkoutFeedback object" = 'object\s+WorkoutFeedback' "static sound bridge" = '(?s)@JvmStatic\s+fun\s+play\s*\(' "static keep-screen bridge" = '(?s)@JvmStatic\s+fun\s+setKeepScreenOn\s*\(' "vibration implementation" = 'VibrationEffect|VibratorManager' } foreach ($entry in $requiredFeedbackPatterns.GetEnumerator()) { if ($workoutFeedbackSource -notmatch $entry.Value) { throw "WorkoutFeedback.kt is missing $($entry.Key)" } } $reminderPath = Join-Path $PSScriptRoot "app\android\src\org\bodyweightbase\android\WorkoutReminderWorker.kt" if (-not (Test-Path -LiteralPath $reminderPath)) { throw "Android workout reminder worker was not found: $reminderPath" } $reminderSource = Get-Content -LiteralPath $reminderPath -Raw $requiredReminderPatterns = [ordered]@{ "missed-workout motivation" = 'missedCount\s*>=\s*3' "five-minute action" = 'addAction\([^\r\n]+"Начать 5 минут"' "follow-up work" = 'OneTimeWorkRequestBuilder<WorkoutReminderWorker>' "late follow-up guard" = 'next\.hour\s*>=\s*23' "saved-session suppression" = 'sessionDates\.contains\(today\)' "profile deep link" = 'putExtra\("workoutProfileId",\s*profileId\)' "plan deep link" = 'putExtra\("workoutPlanId",\s*reminder\.plan\.id\)' "thirty-minute action" = '"Через 30 минут"' "evening action" = '"Сегодня вечером"[\s\S]+"Завтра вечером"' "daily action cap" = 'MAX_RESCHEDULES_PER_DAY\s*=\s*2' "manual reminder work" = 'MANUAL_WORK_PREFIX' } $reminderReceiverPath = Join-Path $PSScriptRoot "app\android\src\org\bodyweightbase\android\WorkoutReminderActionReceiver.kt" if (-not (Test-Path -LiteralPath $reminderReceiverPath)) { throw "Android reminder action receiver was not found: $reminderReceiverPath" } $reminderReceiverSource = Get-Content -LiteralPath $reminderReceiverPath -Raw if ($reminderReceiverSource -notmatch 'class\s+WorkoutReminderActionReceiver\s*:\s*BroadcastReceiver' -or $reminderReceiverSource -notmatch 'handleReminderAction') { throw "Android reminder action receiver is incomplete" } foreach ($entry in $requiredReminderPatterns.GetEnumerator()) { if ($reminderSource -notmatch $entry.Value) { throw "WorkoutReminderWorker.kt is missing $($entry.Key)" } } $mainActivityPath = Join-Path $PSScriptRoot "app\android\src\org\bodyweightbase\android\MainActivity.kt" if (-not (Test-Path -LiteralPath $mainActivityPath)) { throw "Android main activity was not found: $mainActivityPath" } $mainActivitySource = Get-Content -LiteralPath $mainActivityPath -Raw foreach ($pattern in @( 'class MainActivity\s*:\s*QtActivity', 'captureWorkoutLaunch\(intent\)', 'override fun onNewIntent', 'fun consumeWorkoutLaunch', 'pendingWorkoutLaunch\s*=\s*""', 'intent\.removeExtra\("openWorkout"\)' 'startQuickWorkout' )) { if ($mainActivitySource -notmatch $pattern) { throw "MainActivity.kt is missing workout launch handling: $pattern" } } $manifestPath = Join-Path $PSScriptRoot "app\android\AndroidManifest.xml" $manifestSource = Get-Content -LiteralPath $manifestPath -Raw if ($manifestSource -notmatch 'android:name="org\.bodyweightbase\.android\.MainActivity"') { throw "AndroidManifest.xml does not use MainActivity" } if ($manifestSource -notmatch 'android:name="org\.bodyweightbase\.android\.WorkoutReminderActionReceiver"') { throw "AndroidManifest.xml does not register WorkoutReminderActionReceiver" } $mainCppPath = Join-Path $PSScriptRoot "app\main.cpp" $mainCppSource = Get-Content -LiteralPath $mainCppPath -Raw if ($mainCppSource -notmatch 'workoutLaunchTimer\.setInterval\(500\)' -or $mainCppSource -notmatch 'setProperty\("currentTab",\s*0\)' -or $mainCppSource -notmatch 'controller\.startQuickWorkout\(0\)') { throw "main.cpp is missing active-app workout launch routing" } $controllerPath = Join-Path $PSScriptRoot "app\session_controller.cpp" $controllerSource = Get-Content -LiteralPath $controllerPath -Raw foreach ($pattern in @( 'void SessionController::startQuickWorkout\(int roundCount\)', 'startWithQuickCheckpoint', 'void SessionController::continueQuickWorkout\(\)', 'void SessionController::finishQuickWorkout\(\)', 'variantId\s*=\s*QStringLiteral\("quick-start"\)', 'sessionMode\s*==\s*QStringLiteral\("quick"\)', 'Пятиминутка сохранена' )) { if ($controllerSource -notmatch $pattern) { throw "SessionController is missing quick-workout behavior: $pattern" } } $mobileQmlPath = Join-Path $PSScriptRoot "app\MobileMain.qml" $mobileQmlSource = Get-Content -LiteralPath $mobileQmlPath -Raw if ($mobileQmlSource -notmatch 'Быстрый старт.+5 минут' -or $mobileQmlSource -notmatch 'sessionController\.startQuickWorkout\(app\.workoutRoundCount\)' -or $mobileQmlSource -notmatch 'sessionController\.continueQuickWorkout\(\)' -or $mobileQmlSource -notmatch 'sessionController\.finishQuickWorkout\(\)') { throw "MobileMain.qml is missing the five-minute quick start" } if ($mobileQmlSource -notmatch 'weeklyGoalProgress' -or $mobileQmlSource -notmatch 'effectiveWeeklyGoalMin' -or $controllerSource -notmatch 'rescueWeeklyGoalMin') { throw "Profile-specific weekly goals are missing" } $sessionControllerPath = Join-Path $PSScriptRoot "app\session_controller.cpp" $sessionControllerSource = Get-Content -LiteralPath $sessionControllerPath -Raw $requiredJniPatterns = [ordered]@{ "WorkoutFeedback JNI class" = '"org/bodyweightbase/android/WorkoutFeedback"' "sound JNI method" = '(?s)"play"\s*,\s*"\(Landroid/content/Context;II\)V"' "keep-screen JNI method" = '(?s)"setKeepScreenOn"\s*,\s*"\(Landroid/content/Context;Z\)V"' } foreach ($entry in $requiredJniPatterns.GetEnumerator()) { if ($sessionControllerSource -notmatch $entry.Value) { throw "SessionController is missing $($entry.Key)" } } $manifest = [xml](Get-Content -LiteralPath (Join-Path $PSScriptRoot "app\android\AndroidManifest.xml") -Raw) $expectedVersion = $manifest.manifest.versionName $badging = (& $AaptPath dump badging $ApkPath) -join "`n" if ($LASTEXITCODE -ne 0) { throw "aapt failed to inspect APK" } if ($badging -notmatch "package: name='$([regex]::Escape($PackageName))'") { throw "Unexpected APK package name" } if ($badging -notmatch "versionName='$([regex]::Escape($expectedVersion))'") { throw "Unexpected APK version" } if ($badging -notmatch "native-code: 'arm64-v8a'") { throw "APK does not declare arm64-v8a native code" } $requiredPermissions = @( "android.permission.health.READ_STEPS", "android.permission.health.READ_SLEEP", "android.permission.health.READ_HEART_RATE", "android.permission.health.READ_RESTING_HEART_RATE", "android.permission.health.READ_WEIGHT", "android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND", "android.permission.health.WRITE_EXERCISE", "android.permission.POST_NOTIFICATIONS", "android.permission.VIBRATE" ) foreach ($permission in $requiredPermissions) { if ($badging -notmatch "uses-permission: name='$([regex]::Escape($permission))'") { throw "APK is missing permission: $permission" } } $apk = Get-Item -LiteralPath $ApkPath $hash = (Get-FileHash -LiteralPath $ApkPath -Algorithm SHA256).Hash $hashPath = "$ApkPath.sha256" if (Test-Path -LiteralPath $hashPath) { $expectedHash = ((Get-Content -LiteralPath $hashPath -Raw).Trim() -split '\s+')[0] if ($hash -ne $expectedHash) { throw "APK SHA256 mismatch" } } $deviceVerified = $false $stateUnchanged = $null if ($Install) { if (-not (Test-Path -LiteralPath $AdbPath)) { throw "adb was not found: $AdbPath" } $devices = @(& $AdbPath devices | Select-String "\tdevice$" | ForEach-Object { ($_.Line -split "\t")[0] }) if ([string]::IsNullOrWhiteSpace($DeviceId)) { if ($devices.Count -ne 1) { throw "Expected exactly one Android device, found $($devices.Count)" } $DeviceId = $devices[0] } elseif ($DeviceId -notin $devices) { throw "Android device is not connected: $DeviceId" } $adbArgs = @('-s', $DeviceId) $beforeState = (& $AdbPath @adbArgs shell run-as $PackageName cat files/native-state.json 2>$null) -join "`n" $beforeHash = if ($beforeState) { Get-TextSha256 $beforeState } else { "" } $installOutput = (& $AdbPath @adbArgs install -r $ApkPath) -join "`n" if ($LASTEXITCODE -ne 0 -or $installOutput -notmatch "Success") { throw "APK installation failed: $installOutput" } $packageDump = (& $AdbPath @adbArgs shell dumpsys package $PackageName) -join "`n" if ($packageDump -notmatch "versionName=$([regex]::Escape($expectedVersion))") { throw "Installed version does not match APK" } $afterState = (& $AdbPath @adbArgs shell run-as $PackageName cat files/native-state.json 2>$null) -join "`n" $afterHash = if ($afterState) { Get-TextSha256 $afterState } else { "" } $stateUnchanged = $beforeHash -eq $afterHash if (-not $stateUnchanged) { throw "Installing APK changed native-state.json" } $deviceVerified = $true } [pscustomobject]@{ Apk = [System.IO.Path]::GetFullPath($ApkPath) Version = $expectedVersion SizeMB = [math]::Round($apk.Length / 1MB, 2) Sha256 = $hash Arm64 = $true RequiredPermissions = $requiredPermissions.Count WorkoutFeedbackBridge = $true DeviceInstalled = $deviceVerified NativeStateUnchanged = $stateUnchanged } | ConvertTo-Json -Depth 3