<# .SYNOPSIS Builds, verifies, and installs a signed Memby release on an Android TV. .DESCRIPTION Uses the permanent signing configuration stored in the current Windows user's MEMBY_KEYSTORE environment variables. The APK is installed directly with ADB; no Gitea release or update server is required. .EXAMPLE .\deploy-tv.ps1 .EXAMPLE .\deploy-tv.ps1 -Device 10.0.0.238:35343 -SkipTests #> #Requires -Version 7.2 [CmdletBinding()] param( [Parameter()] [ValidatePattern('^[A-Za-z0-9._:-]+$')] [string] $Device = '10.0.0.238:35343', [Parameter()] [switch] $SkipTests ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $root = $PSScriptRoot $packageName = 'com.ponzischeme89.memby' function Invoke-Checked { param( [Parameter(Mandatory)] [string] $FilePath, [Parameter()] [string[]] $Arguments = @() ) & $FilePath @Arguments if ($LASTEXITCODE -ne 0) { throw "'$FilePath' failed with exit code $LASTEXITCODE." } } function Get-AndroidSdk { if ($env:ANDROID_HOME -and (Test-Path -LiteralPath $env:ANDROID_HOME)) { return $env:ANDROID_HOME } $propertiesPath = Join-Path $root 'local.properties' if (Test-Path -LiteralPath $propertiesPath) { $sdkLine = Get-Content -LiteralPath $propertiesPath | Where-Object { $_ -match '^sdk\.dir=' } | Select-Object -First 1 if ($sdkLine) { $sdkPath = $sdkLine.Substring($sdkLine.IndexOf('=') + 1). Replace('\:', ':'). Replace('\\', '\') if (Test-Path -LiteralPath $sdkPath) { return $sdkPath } } } throw 'Android SDK not found. Set ANDROID_HOME or sdk.dir in local.properties.' } function Import-UserSigningEnvironment { foreach ($name in @( 'MEMBY_KEYSTORE', 'MEMBY_KEYSTORE_PASSWORD', 'MEMBY_KEY_ALIAS', 'MEMBY_KEY_PASSWORD' )) { $value = [Environment]::GetEnvironmentVariable($name, 'Process') if ([string]::IsNullOrWhiteSpace($value)) { $value = [Environment]::GetEnvironmentVariable($name, 'User') } if ([string]::IsNullOrWhiteSpace($value)) { throw "Release signing variable $name is not configured." } [Environment]::SetEnvironmentVariable($name, $value, 'Process') } if (-not (Test-Path -LiteralPath $env:MEMBY_KEYSTORE -PathType Leaf)) { throw "Release keystore not found: $env:MEMBY_KEYSTORE" } } Write-Host '' Write-Host '╭─ Memby TV deployment' -ForegroundColor Magenta Write-Host "│ Device $Device" -ForegroundColor DarkGray Write-Host '│ Build signed release' -ForegroundColor DarkGray Write-Host '╰─' -ForegroundColor Magenta Write-Host '' Import-UserSigningEnvironment $sdk = Get-AndroidSdk $adb = Join-Path $sdk 'platform-tools\adb.exe' if (-not (Test-Path -LiteralPath $adb -PathType Leaf)) { throw "ADB not found: $adb" } $buildTools = Get-ChildItem -LiteralPath (Join-Path $sdk 'build-tools') -Directory | Sort-Object { [version]$_.Name } -Descending | Select-Object -First 1 if (-not $buildTools) { throw 'Android SDK Build Tools are not installed.' } $apkSigner = Join-Path $buildTools.FullName 'apksigner.bat' if (-not (Test-Path -LiteralPath $apkSigner -PathType Leaf)) { throw "APK signer not found: $apkSigner" } $jdkHome = 'C:\Program Files\Android\Android Studio\jbr' if (Test-Path -LiteralPath $jdkHome) { $env:JAVA_HOME = $jdkHome } Write-Host '● Building release APK' -ForegroundColor Cyan $gradleArguments = @('--console=plain') if (-not $SkipTests) { $gradleArguments += 'testDebugUnitTest' } $gradleArguments += 'assembleRelease' Invoke-Checked -FilePath (Join-Path $root 'gradlew.bat') -Arguments $gradleArguments $apk = Join-Path $root 'app\build\outputs\apk\release\app-release.apk' if (-not (Test-Path -LiteralPath $apk -PathType Leaf)) { $unsigned = Join-Path $root 'app\build\outputs\apk\release\app-release-unsigned.apk' if (Test-Path -LiteralPath $unsigned) { throw 'Gradle produced an unsigned APK; deployment was stopped.' } throw "Release APK not found: $apk" } Write-Host '' Write-Host '● Verifying release signature' -ForegroundColor Cyan Invoke-Checked -FilePath $apkSigner -Arguments @( 'verify', '--verbose', '--print-certs', $apk ) Write-Host '' Write-Host '● Connecting to Android TV' -ForegroundColor Cyan & $adb connect $Device if ($LASTEXITCODE -ne 0) { throw "Unable to connect to Android TV at $Device." } Invoke-Checked -FilePath $adb -Arguments @('-s', $Device, 'get-state') Write-Host '' Write-Host '● Installing release' -ForegroundColor Cyan Invoke-Checked -FilePath $adb -Arguments @( '-s', $Device, 'install', '-r', '--no-streaming', $apk ) $packageDetails = & $adb -s $Device shell dumpsys package $packageName if ($LASTEXITCODE -ne 0) { throw 'The APK installed, but its package details could not be read.' } $versionName = ( $packageDetails | Select-String -Pattern 'versionName=([^\s]+)' | Select-Object -First 1 ).Matches.Groups[1].Value if ([string]::IsNullOrWhiteSpace($versionName)) { throw "Installed package $packageName was not found." } $resolveOutput = & $adb -s $Device shell cmd package resolve-activity --brief ` -a android.intent.action.MAIN ` -c android.intent.category.LEANBACK_LAUNCHER ` $packageName $component = $resolveOutput | Where-Object { $_.Trim() -match '^[A-Za-z0-9._]+/[A-Za-z0-9._$]+$' } | Select-Object -Last 1 if ($LASTEXITCODE -eq 0 -and $component) { $component = $component.Trim() Invoke-Checked -FilePath $adb -Arguments @( '-s', $Device, 'shell', 'am', 'start', '-n', $component ) } Write-Host '' Write-Host "✓ Memby $versionName installed on $Device" -ForegroundColor Green Write-Host " Package: $packageName" -ForegroundColor DarkGray Write-Host " APK: $apk" -ForegroundColor DarkGray