diff --git a/.github/scripts/BuildAndRunHostApp.ps1 b/.github/scripts/BuildAndRunHostApp.ps1
index 0702c73b015a..b349753980eb 100644
--- a/.github/scripts/BuildAndRunHostApp.ps1
+++ b/.github/scripts/BuildAndRunHostApp.ps1
@@ -232,6 +232,28 @@ if ($Category) {
if ($Platform -eq "android") {
Write-Info "Clearing Android logcat buffer before test..."
& adb -s $DeviceUdid logcat -c
+
+ # Dismiss any ANR dialogs that may have appeared during build/deploy.
+ # The emulator can sit idle during long builds, causing SystemUI ANR.
+ Write-Info "Dismissing any system dialogs before test..."
+ & adb -s $DeviceUdid shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>$null
+ & adb -s $DeviceUdid shell input keyevent KEYCODE_ENTER 2>$null
+ & adb -s $DeviceUdid shell input keyevent KEYCODE_BACK 2>$null
+ Start-Sleep -Seconds 1
+ & adb -s $DeviceUdid shell input keyevent KEYCODE_WAKEUP 2>$null
+ & adb -s $DeviceUdid shell input keyevent KEYCODE_MENU 2>$null
+ Start-Sleep -Seconds 1
+
+ # Check for lingering ANR dialogs via window dump
+ $windowDump = & adb -s $DeviceUdid shell dumpsys window 2>$null | Select-String "Application Not Responding|ANR"
+ if ($windowDump) {
+ Write-Warn "ANR dialog detected — force-dismissing..."
+ & adb -s $DeviceUdid shell input keyevent KEYCODE_HOME 2>$null
+ Start-Sleep -Seconds 2
+ & adb -s $DeviceUdid shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>$null
+ & adb -s $DeviceUdid shell input keyevent KEYCODE_BACK 2>$null
+ Start-Sleep -Seconds 1
+ }
}
# Capture test start time for iOS logs
diff --git a/.github/scripts/Review-PR.ps1 b/.github/scripts/Review-PR.ps1
index 9a83d73634b2..4b96d435f8c1 100644
--- a/.github/scripts/Review-PR.ps1
+++ b/.github/scripts/Review-PR.ps1
@@ -131,12 +131,14 @@ if (-not $ghVersion) {
}
Write-Host " ✅ GitHub CLI: $ghVersion" -ForegroundColor Green
-# Check Copilot CLI
-$copilotVersion = copilot --version 2>$null
-if (-not $copilotVersion) {
+# Check Copilot CLI - use Get-Command (reliable) then get version with merged streams
+$copilotCmd = Get-Command copilot -ErrorAction SilentlyContinue
+if (-not $copilotCmd) {
Write-Error "Copilot CLI is not installed. Install with: npm install -g @github/copilot"
exit 1
}
+$copilotVersion = (& copilot --version 2>&1 | Out-String).Trim()
+if (-not $copilotVersion) { $copilotVersion = $copilotCmd.Source }
Write-Host " ✅ Copilot CLI: $copilotVersion" -ForegroundColor Green
# Check PR exists
diff --git a/.github/scripts/shared/Build-AndDeploy.ps1 b/.github/scripts/shared/Build-AndDeploy.ps1
index f57d5c088094..ae81e05a1ea8 100644
--- a/.github/scripts/shared/Build-AndDeploy.ps1
+++ b/.github/scripts/shared/Build-AndDeploy.ps1
@@ -85,19 +85,56 @@ if ($Platform -eq "android") {
Write-Info "Build command: dotnet build $($buildArgs -join ' ')"
$buildStartTime = Get-Date
+ $maxAttempts = 2
+ $buildExitCode = 1
+
+ for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
+ if ($attempt -gt 1) {
+ Write-Warn "Retrying build/deploy (attempt $attempt of $maxAttempts)..."
+
+ # Uninstall any MAUI test packages to clear bad state
+ $installedPkg = & adb shell pm list packages 2>$null | Select-String "maui" | ForEach-Object { ($_ -replace "package:", "").Trim() }
+ if ($installedPkg) {
+ foreach ($pkg in $installedPkg) {
+ Write-Info "Uninstalling $pkg before retry..."
+ & adb uninstall $pkg 2>$null
+ }
+ }
+
+ # Restart ADB server to recover from broken pipe / transient errors
+ Write-Info "Restarting ADB server..."
+ & adb kill-server 2>$null
+ Start-Sleep -Seconds 2
+ & adb start-server
+ Start-Sleep -Seconds 2
+ & adb wait-for-device
+ Start-Sleep -Seconds 3
+ }
+
+ & dotnet build @buildArgs
+ $buildExitCode = $LASTEXITCODE
+
+ if ($buildExitCode -eq 0) {
+ break
+ }
+
+ if ($attempt -lt $maxAttempts) {
+ Write-Warn "Build/deploy failed (attempt $attempt). ADB0010/broken-pipe errors are transient on API 30 — will retry."
+ }
+ }
- # Build and deploy in one step (Run target handles both)
- & dotnet build @buildArgs
-
- $buildExitCode = $LASTEXITCODE
$buildDuration = (Get-Date) - $buildStartTime
if ($buildExitCode -ne 0) {
- Write-Error "Build/deploy failed with exit code $buildExitCode"
+ Write-Error "Build/deploy failed after $maxAttempts attempts with exit code $buildExitCode"
exit $buildExitCode
}
- Write-Success "Build and deploy completed in $($buildDuration.TotalSeconds) seconds"
+ if ($attempt -gt 1) {
+ Write-Success "Build and deploy succeeded on attempt $attempt in $($buildDuration.TotalSeconds) seconds"
+ } else {
+ Write-Success "Build and deploy completed in $($buildDuration.TotalSeconds) seconds"
+ }
#endregion
diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1
index a5d90f192530..b1d7875326ba 100644
--- a/.github/scripts/shared/Start-Emulator.ps1
+++ b/.github/scripts/shared/Start-Emulator.ps1
@@ -65,7 +65,8 @@ if ($Platform -eq "android") {
# Check if DeviceUdid is an AVD name (not an emulator-XXXX format)
if ($DeviceUdid -and $DeviceUdid -notmatch "^emulator-\d+$") {
# DeviceUdid is likely an AVD name - check if it's in the AVD list
- $avdList = emulator -list-avds 2>$null
+ # Force array output - single AVD returns a string which breaks -contains
+ [string[]]$avdList = @(emulator -list-avds 2>$null)
if ($avdList -contains $DeviceUdid) {
Write-Info "DeviceUdid '$DeviceUdid' is an AVD name. Will boot this emulator..."
$selectedAvd = $DeviceUdid
@@ -103,7 +104,8 @@ if ($Platform -eq "android") {
# Get list of available AVDs (if not already set from parameter)
if (-not $selectedAvd) {
- $avdList = emulator -list-avds 2>$null
+ # Force array output - single AVD returns a string which breaks indexing
+ [string[]]$avdList = @(emulator -list-avds 2>$null)
if (-not $avdList -or $avdList.Count -eq 0) {
Write-Error "No Android emulators found. Please create an Android Virtual Device (AVD) using Android Studio."
@@ -119,7 +121,7 @@ if ($Platform -eq "android") {
# Selection priority:
# 1. API 34 device (matches CI provisioning)
# 2. API 30 Nexus device
- # 3. Any API 30 device
+ # 3. Any API 30 device (matches names like "Emulator_30", "API_30_xxx", etc.)
# 4. Any Nexus device
# 5. First available device
@@ -132,16 +134,16 @@ if ($Platform -eq "android") {
# Try to find API 30 Nexus device
if (-not $selectedAvd) {
- $api30Nexus = $avdList | Where-Object { $_ -match "API.*30" -and $_ -match "Nexus" } | Select-Object -First 1
+ $api30Nexus = $avdList | Where-Object { $_ -match "30" -and $_ -match "Nexus" } | Select-Object -First 1
if ($api30Nexus) {
$selectedAvd = $api30Nexus
Write-Info "Selected API 30 Nexus device: $selectedAvd"
}
}
- # Try to find any API 30 device
+ # Try to find any API 30 device (match "30" anywhere in name)
if (-not $selectedAvd) {
- $api30Device = $avdList | Where-Object { $_ -match "API.*30" } | Select-Object -First 1
+ $api30Device = $avdList | Where-Object { $_ -match "30" } | Select-Object -First 1
if ($api30Device) {
$selectedAvd = $api30Device
Write-Info "Selected API 30 device: $selectedAvd"
diff --git a/eng/pipelines/ci-copilot.yml b/eng/pipelines/ci-copilot.yml
index 05e4d11a2796..b8c8c4d4399f 100644
--- a/eng/pipelines/ci-copilot.yml
+++ b/eng/pipelines/ci-copilot.yml
@@ -26,7 +26,8 @@ parameters:
- name: pool
type: object
default:
- name: AcesShared
+ name: Azure Pipelines
+ vmImage: ubuntu-22.04
variables:
- template: /eng/pipelines/common/variables.yml@self
@@ -34,6 +35,10 @@ variables:
value: false
- name: Codeql.SkipTaskAutoInjection
value: true
+ - name: APPIUM_HOME
+ value: $(System.DefaultWorkingDirectory)/.appium/
+ - name: LogDirectory
+ value: $(Build.ArtifactStagingDirectory)/logs
stages:
- stage: ReviewPR
@@ -42,7 +47,7 @@ stages:
- job: CopilotReview
displayName: 'Run Copilot PR Reviewer Agent'
pool: ${{ parameters.pool }}
- timeoutInMinutes: 180
+ timeoutInMinutes: 360
steps:
- checkout: self
fetchDepth: 0
@@ -61,135 +66,24 @@ stages:
echo "##vso[build.updatebuildnumber]PR ${{ parameters.PRNumber }} ${{ parameters.Platform }}"
displayName: 'Set Pipeline Run Title'
- # Provision environment (Xcode, .NET SDK, Android SDK, etc.)
+ # Enable KVM for Android emulator on Linux (same as ui-tests-steps.yml / device-tests-steps.yml)
+ - ${{ if eq(parameters.Platform, 'android') }}:
+ - template: common/enable-kvm.yml
+
+ # Provision SDKs (same parameters as ui-tests-steps.yml)
- template: common/provision.yml
parameters:
- skipXcode: false
- skipProvisionator: false
- skipAndroidCommonSdks: ${{ eq(parameters.Platform, 'ios') }}
- skipAndroidPlatformApis: ${{ eq(parameters.Platform, 'ios') }}
- skipJdk: ${{ eq(parameters.Platform, 'ios') }}
- skipSimulatorSetup: false
- skipCertificates: true
- # Android emulator setup (skip for iOS)
- skipAndroidEmulatorImages: ${{ eq(parameters.Platform, 'ios') }}
- skipAndroidCreateAvds: ${{ eq(parameters.Platform, 'ios') }}
+ skipXcode: ${{ eq(parameters.Platform, 'android') }}
+ skipProvisionator: true
+ skipJdk: ${{ ne(parameters.Platform, 'android') }}
+ skipAndroidCommonSdks: ${{ ne(parameters.Platform, 'android') }}
+ skipAndroidPlatformApis: true
+ onlyAndroidPlatformDefaultApis: true
+ skipAndroidEmulatorImages: ${{ ne(parameters.Platform, 'android') }}
+ skipAndroidCreateAvds: true
androidEmulatorApiLevel: '30'
-
- # Configure AVD for hardware acceleration (AcesShared ARM64 macOS agents have HVF)
- # Match maui-pr Cake script: no GPU override on macOS (uses default hw GPU)
- - script: |
- echo "=== Configuring AVD for hardware-accelerated emulation ==="
- AVD_CONFIG="$HOME/.android/avd/Emulator_30.avd/config.ini"
- if [ -f "$AVD_CONFIG" ]; then
- echo "Found AVD config: $AVD_CONFIG"
- cat "$AVD_CONFIG"
- else
- echo "##vso[task.logissue type=warning]AVD config not found at: $AVD_CONFIG"
- ls -la "$HOME/.android/avd/" 2>/dev/null || echo "AVD directory not found"
- fi
- displayName: 'Configure AVD'
- condition: eq('${{ parameters.Platform }}', 'android')
-
- # Set up Android SDK PATH (required on self-hosted agents)
- - pwsh: |
- if ($env:ANDROID_SDK_ROOT) {
- $platformTools = Join-Path $env:ANDROID_SDK_ROOT "platform-tools"
- $emulatorPath = Join-Path $env:ANDROID_SDK_ROOT "emulator"
- $cmdlineTools = Join-Path $env:ANDROID_SDK_ROOT "cmdline-tools/latest/bin"
-
- # Use Azure Pipelines' prependpath command to persist across steps
- Write-Host "##vso[task.prependpath]$platformTools"
- Write-Host "##vso[task.prependpath]$emulatorPath"
- Write-Host "##vso[task.prependpath]$cmdlineTools"
-
- Write-Host "Added to PATH (will apply to subsequent steps):"
- Write-Host " platform-tools: $platformTools"
- Write-Host " emulator: $emulatorPath"
- Write-Host " cmdline-tools: $cmdlineTools"
-
- # Verify the tools exist
- if (Test-Path (Join-Path $platformTools "adb")) {
- Write-Host "✅ adb found at: $platformTools/adb"
- } else {
- Write-Host "⚠️ adb NOT found at expected location"
- }
- if (Test-Path (Join-Path $emulatorPath "emulator")) {
- Write-Host "✅ emulator found at: $emulatorPath/emulator"
- } else {
- Write-Host "⚠️ emulator NOT found at expected location"
- }
- } else {
- Write-Host "##vso[task.logissue type=warning]ANDROID_SDK_ROOT not set, skipping PATH setup"
- }
-
- # Auto-detect and set JAVA_HOME if not already set
- if (-not $env:JAVA_HOME) {
- $jvmDir = "/Library/Java/JavaVirtualMachines"
- $msJdk = Get-ChildItem "$jvmDir/microsoft-*.jdk" -ErrorAction SilentlyContinue | Select-Object -First 1
- if ($msJdk) {
- $javaHome = "$($msJdk.FullName)/Contents/Home"
- Write-Host "##vso[task.setvariable variable=JAVA_HOME]$javaHome"
- Write-Host "Set JAVA_HOME to: $javaHome"
- }
- }
- displayName: 'Configure Android SDK PATH'
- condition: eq('${{ parameters.Platform }}', 'android')
- env:
- ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT)
-
- # Start Android Emulator EARLY (using Start-Emulator.ps1 script)
- - pwsh: |
- Write-Host "=== Starting Android Emulator ==="
- Write-Host "Working directory: $(Get-Location)"
- Write-Host "ANDROID_SDK_ROOT: $env:ANDROID_SDK_ROOT"
- Write-Host "JAVA_HOME: $env:JAVA_HOME"
- Write-Host "PATH (first 500 chars): $($env:PATH.Substring(0, [Math]::Min(500, $env:PATH.Length)))"
-
- # Verify adb is in path
- $adbPath = Get-Command adb -ErrorAction SilentlyContinue
- if ($adbPath) {
- Write-Host "adb found at: $($adbPath.Source)"
- } else {
- Write-Host "adb NOT in PATH, checking manually..."
- $manualAdb = Join-Path $env:ANDROID_SDK_ROOT "platform-tools/adb"
- if (Test-Path $manualAdb) {
- Write-Host "adb exists at $manualAdb but not in PATH"
- }
- }
-
- $scriptPath = ".github/scripts/shared/Start-Emulator.ps1"
- Write-Host "Script path: $scriptPath"
-
- if (-not (Test-Path $scriptPath)) {
- Write-Host "##vso[task.logissue type=error]Script not found: $scriptPath"
- exit 1
- }
- Write-Host "Script exists: OK"
-
- $ErrorActionPreference = "Continue"
- try {
- Write-Host "Invoking Start-Emulator.ps1 -Platform android -DeviceUdid Emulator_30..."
- & "./$scriptPath" -Platform android -DeviceUdid Emulator_30 2>&1 | ForEach-Object { Write-Host $_ }
- $exitCode = $LASTEXITCODE
- Write-Host "Script returned exit code: $exitCode"
- } catch {
- Write-Host "##vso[task.logissue type=error]Exception: $_"
- Write-Host $_.ScriptStackTrace
- $exitCode = 1
- }
-
- if ($exitCode -ne 0) {
- Write-Host "##vso[task.logissue type=error]Start-Emulator failed with code $exitCode"
- exit $exitCode
- }
-
- Write-Host "=== Android Emulator Started Successfully ==="
- displayName: 'Start Android Emulator'
- condition: eq('${{ parameters.Platform }}', 'android')
- timeoutInMinutes: 35
- env:
- ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT)
+ skipSimulatorSetup: ${{ eq(parameters.Platform, 'android') }}
+ skipCertificates: true
# Install .NET and workloads via build.ps1
- pwsh: ./build.ps1 --target=dotnet --configuration="Release" --verbosity=diagnostic
@@ -210,214 +104,186 @@ stages:
DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token)
PRIVATE_BUILD: $(PrivateBuild)
- # Verify environment is ready
- - script: |
- echo "=== Verifying Build Environment ==="
- ERRORS=""
-
- # Check .NET SDK
- echo "Checking .NET SDK..."
- if ! dotnet --version; then
- ERRORS="${ERRORS}\n- .NET SDK not available"
- else
- echo "✓ .NET SDK: $(dotnet --version)"
- fi
-
- # Check Android SDK (only for Android platform)
- if [ "${{ parameters.Platform }}" = "android" ]; then
- echo "Checking Android SDK..."
- if [ -z "$ANDROID_HOME" ] && [ -z "$ANDROID_SDK_ROOT" ]; then
- ERRORS="${ERRORS}\n- ANDROID_HOME/ANDROID_SDK_ROOT not set"
- else
- SDK_PATH="${ANDROID_HOME:-$ANDROID_SDK_ROOT}"
- if [ ! -d "$SDK_PATH" ]; then
- ERRORS="${ERRORS}\n- Android SDK directory not found: $SDK_PATH"
- else
- echo "✓ Android SDK: $SDK_PATH"
- fi
- fi
- fi
-
- # Check Xcode (macOS only)
- if [ "$(uname)" = "Darwin" ]; then
- echo "Checking Xcode..."
- if ! xcodebuild -version; then
- ERRORS="${ERRORS}\n- Xcode not available"
- else
- echo "✓ Xcode available"
- fi
+ # Restore .NET tools (includes xharness)
+ - script: dotnet tool restore
+ displayName: 'Restore .NET Tools'
+
+ # Create AVD and boot Android Emulator
+ - ${{ if eq(parameters.Platform, 'android') }}:
+ # Free disk space on hosted agents (emulator needs ~7GB for userdata partition)
+ - script: |
+ echo "=== Disk space before cleanup ==="
+ df -h /home
+ echo "Removing unnecessary tools to free space..."
+ sudo rm -rf /usr/share/dotnet /usr/local/share/powershell /usr/local/share/chromium 2>/dev/null || true
+ sudo rm -rf /opt/hostedtoolcache/CodeQL /opt/hostedtoolcache/go /opt/hostedtoolcache/Python 2>/dev/null || true
+ sudo rm -rf /usr/share/swift 2>/dev/null || true
+ echo "=== Disk space after cleanup ==="
+ df -h /home
+ displayName: 'Free Disk Space for Emulator'
+
+ - script: |
+ export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}"
+ export PATH="$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/emulator:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$PATH"
+
+ echo "=== Creating AVD ==="
+ echo "no" | avdmanager create avd -n Emulator_30 -k "system-images;android-30;google_apis_playstore;x86_64" --device "Nexus 5X" --force
- # Check iOS simulators
- echo "Checking iOS simulators..."
- if ! xcrun simctl list devices available | grep -q "iPhone"; then
- ERRORS="${ERRORS}\n- No iOS simulators available"
- else
- echo "✓ iOS simulators available"
+ # Reduce userdata partition to fit on hosted agents (~4.2GB free)
+ AVD_CONFIG="$HOME/.android/avd/Emulator_30.avd/config.ini"
+ if [ -f "$AVD_CONFIG" ]; then
+ sed -i 's/disk.dataPartition.size=.*/disk.dataPartition.size=2048m/' "$AVD_CONFIG"
+ echo "Updated disk.dataPartition.size to 2048m"
fi
- fi
-
- # Check Java/JDK (only for Android platform)
- if [ "${{ parameters.Platform }}" = "android" ]; then
- echo "Checking JDK..."
- if ! java -version 2>&1; then
- ERRORS="${ERRORS}\n- JDK not available"
- else
- echo "✓ JDK available"
+
+ # Pre-authorize ADB keys (mirrors android.cake HandleVirtualDevice)
+ echo "=== Pre-authorizing ADB keys ==="
+ mkdir -p "$HOME/.android"
+ if [ ! -f "$HOME/.android/adbkey" ]; then
+ adb keygen "$HOME/.android/adbkey" 2>/dev/null || true
fi
- fi
-
- # Report errors
- if [ -n "$ERRORS" ]; then
- echo ""
- echo "##vso[task.logissue type=error]=== Environment Verification FAILED ==="
- echo -e "Missing dependencies:$ERRORS"
- echo "##vso[task.logissue type=error]Build environment is not properly configured. See above for details."
- exit 1
- fi
-
- echo ""
- echo "=== Environment Verification PASSED ==="
- displayName: 'Verify Build Environment'
+ ADB_KEY_PUB="$HOME/.android/adbkey.pub"
+ AVD_DIR="$HOME/.android/avd/Emulator_30.avd"
+ if [ -f "$ADB_KEY_PUB" ] && [ -d "$AVD_DIR" ]; then
+ cp "$ADB_KEY_PUB" "$AVD_DIR/adbkey.pub"
+ echo "ADB key pre-authorized for emulator"
+ fi
+
+ echo "=== Starting Emulator ==="
+ # Kill any stale adb server and restart
+ adb kill-server 2>/dev/null || true
+ sleep 1
+ adb start-server
+
+ # Retry loop: emulator sometimes fails to connect ADB on first launch
+ MAX_LAUNCH_ATTEMPTS=2
+ EMULATOR_PID=""
+ for LAUNCH_ATTEMPT in $(seq 1 $MAX_LAUNCH_ATTEMPTS); do
+ echo "--- Emulator launch attempt $LAUNCH_ATTEMPT of $MAX_LAUNCH_ATTEMPTS ---"
+ if [ $LAUNCH_ATTEMPT -gt 1 ]; then
+ echo "Cleaning up before retry..."
+ if [ -n "$EMULATOR_PID" ] && kill -0 "$EMULATOR_PID" 2>/dev/null; then
+ kill "$EMULATOR_PID" 2>/dev/null || true
+ sleep 2
+ kill -0 "$EMULATOR_PID" 2>/dev/null && kill -9 "$EMULATOR_PID" 2>/dev/null || true
+ fi
+ sleep 3
+ adb kill-server 2>/dev/null || true
+ sleep 2
+ adb start-server
+ sleep 2
+ fi
- # Restore .NET tools (includes xharness)
- - script: |
- echo "Restoring .NET tools..."
- dotnet tool restore
- echo "Tools restored successfully"
- displayName: 'Restore .NET Tools'
+ nohup emulator -avd Emulator_30 -gpu swiftshader_indirect -no-window -no-snapshot -no-audio -no-boot-anim -partition-size 2048 > /tmp/emulator.log 2>&1 &
+ EMULATOR_PID=$!
+ echo "Emulator PID: $EMULATOR_PID"
+
+ echo "Waiting for emulator device (adb wait-for-device, 120s timeout)..."
+ timeout 120 adb wait-for-device
+ if [ $? -eq 0 ]; then
+ echo "Device detected: $(adb devices -l | grep emulator)"
+ break
+ fi
- # Boot Android emulator for UI tests
- - script: |
- echo "=== Booting Android Emulator ==="
-
- # Match maui-pr Cake script: on macOS use default GPU (hardware-accelerated),
- # only use swiftshader on Linux. AcesShared agents are ARM64 macOS with HVF.
- # Start emulator in background with logging for debugging
- EMULATOR_LOG="/tmp/emulator-boot.log"
- nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim > "$EMULATOR_LOG" 2>&1 &
- EMULATOR_PID=$!
- echo "Emulator started with PID: $EMULATOR_PID"
-
- # Give emulator a moment to start and verify process is running
- sleep 3
- if ! pgrep -f 'qemu-system' > /dev/null; then
- echo "##vso[task.logissue type=error]Emulator process did not start"
- echo "=== Emulator Log ==="
- cat "$EMULATOR_LOG" 2>/dev/null || echo "No log available"
- exit 1
- fi
- echo "Emulator process verified running"
-
- # Wait for device to appear with timeout (don't use adb wait-for-device - can hang forever)
- echo "Waiting for emulator device..."
- device_timeout=60
- device_waited=0
- while ! adb devices | grep -q "emulator.*device"; do
- sleep 2
- device_waited=$((device_waited + 2))
- if [ $device_waited -ge $device_timeout ]; then
- echo "##vso[task.logissue type=error]Emulator device did not appear in time"
- echo "=== Emulator Log (last 50 lines) ==="
- tail -50 "$EMULATOR_LOG" 2>/dev/null || echo "No log available"
+ echo "##vso[task.logissue type=warning]adb wait-for-device timed out (attempt $LAUNCH_ATTEMPT)"
adb devices -l
- exit 1
- fi
- done
- echo "Emulator device detected"
-
- # Wait for boot_completed
- echo "Waiting for emulator to finish booting..."
- timeout=600
- waited=0
- while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do
- sleep 2
- waited=$((waited + 2))
- echo "Waiting for boot... ($waited/$timeout seconds)"
- if [ $waited -ge $timeout ]; then
- echo "##vso[task.logissue type=error]Emulator did not boot in time"
- echo "=== Emulator Log (last 50 lines) ==="
- tail -50 "$EMULATOR_LOG" 2>/dev/null || echo "No log available"
- adb devices -l
- exit 1
- fi
- done
- echo "Boot completed flag set"
-
- # Wait for package manager service to be available (critical for app installation)
- echo "Waiting for package manager service..."
- pm_timeout=120
- pm_waited=0
- while ! adb shell pm list packages 2>/dev/null | grep -q "package:"; do
- sleep 3
- pm_waited=$((pm_waited + 3))
- echo "Waiting for package manager... ($pm_waited/$pm_timeout seconds)"
- if [ $pm_waited -ge $pm_timeout ]; then
- echo "##vso[task.logissue type=error]Package manager service did not start"
- echo "=== Checking services ==="
- adb shell service list 2>/dev/null | head -20 || echo "Cannot list services"
- exit 1
- fi
- done
- echo "Package manager service is ready"
-
- echo "=== Emulator booted successfully! ==="
- adb devices -l
- displayName: 'Boot Android Emulator'
- condition: eq('${{ parameters.Platform }}', 'android')
- continueOnError: true
- timeoutInMinutes: 15
- env:
- ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT)
- JAVA_HOME: $(JAVA_HOME_17_X64)
+ tail -30 /tmp/emulator.log
- - script: |
- echo "Installing Node.js 22..."
- brew install node@22
- brew link --overwrite node@22
- if ! node --version; then
- echo "##vso[task.logissue type=error]Failed to install Node.js"
- exit 1
- fi
- npm --version
- echo "Node.js installed successfully"
+ if [ $LAUNCH_ATTEMPT -eq $MAX_LAUNCH_ATTEMPTS ]; then
+ echo "##vso[task.logissue type=error]Emulator failed to connect after $MAX_LAUNCH_ATTEMPTS attempts"
+ exit 1
+ fi
+ done
+
+ timeout=300
+ waited=0
+ while [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; do
+ sleep 5
+ waited=$((waited + 5))
+ if [ $waited -ge $timeout ]; then
+ echo "##vso[task.logissue type=error]Emulator did not finish booting after ${timeout}s"
+ exit 1
+ fi
+ # At 90 seconds, restart ADB server to recover from auth issues (mirrors android.cake)
+ if [ $waited -eq 90 ]; then
+ echo "Boot taking longer than expected (90/${timeout}s). Restarting ADB server..."
+ adb kill-server 2>/dev/null || true
+ sleep 2
+ adb start-server
+ sleep 2
+ echo "ADB server restarted. Continuing to wait..."
+ fi
+ # Re-ensure ADB keys every 60s during boot (mirrors android.cake PrepareDevice)
+ if [ $((waited % 60)) -eq 0 ] && [ $waited -gt 0 ]; then
+ if [ -f "$ADB_KEY_PUB" ] && [ -d "$AVD_DIR" ]; then
+ cp "$ADB_KEY_PUB" "$AVD_DIR/adbkey.pub" 2>/dev/null || true
+ fi
+ fi
+ done
+ echo "Boot completed, waiting for package manager..."
+
+ timeout=120
+ waited=0
+ while ! adb shell pm list packages 2>/dev/null | grep -q "package:"; do
+ sleep 5
+ waited=$((waited + 5))
+ if [ $waited -ge $timeout ]; then
+ echo "##vso[task.logissue type=error]Package manager not ready after ${timeout}s"
+ exit 1
+ fi
+ done
+
+ DEVICE_ID=$(adb devices | grep "emulator.*device" | awk '{print $1}')
+ echo "✅ Emulator fully booted: $DEVICE_ID"
+
+ # Prepare emulator for CI use — keeps device responsive during idle period
+ echo "=== Preparing emulator for CI ==="
+ # Wait for device to stabilize after boot (transient offline state)
+ for i in $(seq 1 10); do
+ if adb -s $DEVICE_ID shell echo ok 2>/dev/null | grep -q ok; then
+ break
+ fi
+ echo "Device offline, retrying ($i/10)..."
+ sleep 3
+ done
+ # Disable all animations (reduces CPU load and flakiness)
+ adb -s $DEVICE_ID shell settings put global window_animation_scale 0.0
+ adb -s $DEVICE_ID shell settings put global transition_animation_scale 0.0
+ adb -s $DEVICE_ID shell settings put global animator_duration_scale 0.0
+ # Prevent screen from turning off (emulator simulates AC charging)
+ adb -s $DEVICE_ID shell settings put system screen_off_timeout 2147483647
+ adb -s $DEVICE_ID shell svc power stayon true
+ # Wake screen and dismiss any lock screen
+ adb -s $DEVICE_ID shell input keyevent 82
+ sleep 1
+ # Dismiss any "System UI has stopped" or crash dialogs
+ adb -s $DEVICE_ID shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true
+ # Clear logcat buffer so agent sees only fresh logs
+ adb -s $DEVICE_ID logcat -c 2>/dev/null || true
+ echo "Emulator preparation complete"
+
+ echo "##vso[task.setvariable variable=DEVICE_UDID]$DEVICE_ID"
+ echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/platform-tools"
+ echo "##vso[task.prependpath]$ANDROID_SDK_ROOT/emulator"
+ displayName: 'Create AVD and Boot Android Emulator'
+ retryCountOnTaskFailure: 1
+ timeoutInMinutes: 15
+
+ # Install Node.js and Appium (same as ui-tests-steps.yml)
+ - task: UseNode@1
+ inputs:
+ version: "24.x"
displayName: 'Install Node.js'
- - script: |
- echo "Installing Appium and platform driver..."
-
- # Get npm global bin directory and add to PATH
- NPM_BIN=$(npm config get prefix)/bin
- echo "NPM global bin directory: $NPM_BIN"
- export PATH="$NPM_BIN:$PATH"
-
- # Install Appium globally
- npm install -g appium
-
- # Install both drivers — the agent may switch platforms if the bug
- # only affects one platform (e.g., iOS bug triggered via Android run)
- echo "Installing UiAutomator2 driver for Android..."
- appium driver install uiautomator2
- echo "Installing XCUITest driver for iOS..."
- appium driver install xcuitest
-
- # Verify installation
- if ! which appium; then
- echo "##vso[task.logissue type=error]Failed to install Appium"
- exit 1
- fi
-
- APPIUM_PATH=$(which appium)
- echo "Appium path: $APPIUM_PATH"
- echo "Appium version: $(appium --version)"
- echo "Appium drivers installed:"
- appium driver list --installed
-
- # Export PATH for subsequent steps (Azure DevOps specific)
- echo "##vso[task.prependpath]$NPM_BIN"
-
- echo "Appium installed successfully"
+ - pwsh: |
+ $skipAppiumDoctor = if ($IsMacOS -or $IsLinux) { "true" } else { "false" }
+ dotnet build ./src/Provisioning/Provisioning.csproj -t:ProvisionAppium -p:SkipAppiumDoctor="$skipAppiumDoctor" -bl:"$(LogDirectory)/provision-appium.binlog"
displayName: 'Install Appium'
+ retryCountOnTaskFailure: 2
+ timeoutInMinutes: 10
+ env:
+ APPIUM_HOME: $(APPIUM_HOME)
- script: |
echo "Installing GitHub CLI..."
@@ -435,11 +301,18 @@ stages:
echo "##vso[task.logissue type=error]GH_CLI_TOKEN is not set. Please configure the pipeline variable."
exit 1
fi
- echo "$(GH_CLI_TOKEN)" | gh auth login --with-token
- if ! gh auth status; then
- echo "##vso[task.logissue type=error]GitHub CLI authentication failed"
- exit 1
+ # Use GH_TOKEN env var to avoid scope validation issues with newer gh versions
+ export GH_TOKEN="$(GH_CLI_TOKEN)"
+ gh auth status
+ if [ $? -ne 0 ]; then
+ # Fallback: try direct login
+ echo "$(GH_CLI_TOKEN)" | gh auth login --with-token 2>/dev/null || true
+ if ! gh auth status; then
+ echo "##vso[task.logissue type=error]GitHub CLI authentication failed"
+ exit 1
+ fi
fi
+ echo "GitHub CLI authenticated successfully"
displayName: 'Authenticate GitHub CLI'
env:
GH_CLI_TOKEN: $(GH_CLI_TOKEN)
@@ -447,89 +320,14 @@ stages:
- script: |
echo "Installing GitHub Copilot CLI..."
npm install -g @github/copilot
- if ! which copilot; then
- echo "##vso[task.logissue type=error]Failed to install GitHub Copilot CLI"
- exit 1
- fi
+ # Ensure npm global bin is on PATH for subsequent steps (Linux UseNode installs to toolcache)
+ COPILOT_BIN_DIR=$(dirname "$(which copilot)")
+ echo "Copilot binary at: $COPILOT_BIN_DIR/copilot"
+ echo "##vso[task.prependpath]$COPILOT_BIN_DIR"
+ copilot --version || true
echo "Copilot CLI installed successfully"
displayName: 'Install GitHub Copilot CLI'
- # Restart Android emulator to ensure it's fresh before PR Reviewer runs
- # The emulator can become unstable after running for a long time
- - script: |
- echo "=== Restarting Android Emulator for PR Reviewer ==="
-
- # Kill any existing emulator
- echo "Killing existing emulator..."
- pkill -f 'qemu-system' 2>/dev/null || true
- sleep 5
-
- # Restart ADB server
- echo "Restarting ADB server..."
- adb kill-server
- sleep 2
- adb start-server
- sleep 2
-
- # Start fresh emulator
- EMULATOR_LOG="/tmp/emulator-fresh.log"
- echo "Starting fresh emulator..."
- nohup $ANDROID_SDK_ROOT/emulator/emulator -avd Emulator_30 -no-window -no-snapshot -no-audio -no-boot-anim > "$EMULATOR_LOG" 2>&1 &
- EMULATOR_PID=$!
- echo "Emulator started with PID: $EMULATOR_PID"
-
- # Wait for device to appear
- echo "Waiting for emulator device..."
- device_timeout=300
- device_waited=0
- while ! adb devices | grep -q "emulator.*device"; do
- sleep 5
- device_waited=$((device_waited + 5))
- if [ $device_waited -ge $device_timeout ]; then
- echo "##vso[task.logissue type=error]Emulator device did not appear in time"
- cat "$EMULATOR_LOG" 2>/dev/null | tail -30
- exit 1
- fi
- echo "Waiting for device... ($device_waited/$device_timeout seconds)"
- done
- echo "Emulator device detected"
-
- # Wait for boot_completed
- echo "Waiting for boot_completed..."
- boot_timeout=600
- boot_waited=0
- while [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" != "1" ]; do
- sleep 5
- boot_waited=$((boot_waited + 5))
- if [ $boot_waited -ge $boot_timeout ]; then
- echo "##vso[task.logissue type=error]Emulator did not boot in time"
- exit 1
- fi
- done
- echo "Boot completed"
-
- # Wait for package manager
- echo "Waiting for package manager service..."
- pm_timeout=120
- pm_waited=0
- while ! adb shell pm list packages 2>/dev/null | grep -q "package:"; do
- sleep 5
- pm_waited=$((pm_waited + 5))
- if [ $pm_waited -ge $pm_timeout ]; then
- echo "##vso[task.logissue type=error]Package manager service did not start"
- adb shell service list 2>/dev/null | head -20
- exit 1
- fi
- echo "Waiting for package manager... ($pm_waited/$pm_timeout seconds)"
- done
-
- echo "=== Fresh Android Emulator Ready! ==="
- adb devices -l
- displayName: 'Restart Android Emulator (Fresh)'
- condition: eq('${{ parameters.Platform }}', 'android')
- timeoutInMinutes: 15
- env:
- ANDROID_SDK_ROOT: $(ANDROID_SDK_ROOT)
# Boot iOS Simulator (only for iOS platform)
# UI test baseline screenshots are captured on iPhone Xs - must use same device
@@ -623,10 +421,86 @@ stages:
condition: eq('${{ parameters.Platform }}', 'ios')
timeoutInMinutes: 5
+ # Warm up the emulator right before the agent runs.
+ # The emulator may have been idle for 15-30 min while Appium/Node/CLI were installed.
+ # Without this, SystemUI can ANR when the agent first touches it.
+ - script: |
+ set -e
+ DEVICE_ID="$(DEVICE_UDID)"
+ if [ -z "$DEVICE_ID" ]; then
+ echo "No DEVICE_UDID set — skipping warmup"
+ exit 0
+ fi
+
+ echo "=== Emulator warmup before agent ==="
+ # Verify device is still connected
+ if ! adb -s "$DEVICE_ID" shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then
+ echo "Device not responding. Restarting ADB server..."
+ adb kill-server 2>/dev/null || true
+ sleep 2
+ adb start-server
+ sleep 2
+ timeout 60 adb wait-for-device
+ fi
+
+ # Dismiss ANR dialogs and wake screen — run twice for reliability
+ for PASS in 1 2; do
+ echo "--- Warmup pass $PASS ---"
+ adb -s "$DEVICE_ID" shell input keyevent KEYCODE_WAKEUP 2>/dev/null || true
+ adb -s "$DEVICE_ID" shell input keyevent KEYCODE_MENU 2>/dev/null || true
+ sleep 1
+
+ # Dismiss system dialogs (ANR, crash, etc.)
+ adb -s "$DEVICE_ID" shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true
+ adb -s "$DEVICE_ID" shell input keyevent KEYCODE_ENTER 2>/dev/null || true
+ adb -s "$DEVICE_ID" shell input keyevent KEYCODE_BACK 2>/dev/null || true
+ sleep 1
+ done
+
+ # Check for lingering ANR in window state
+ if adb -s "$DEVICE_ID" shell dumpsys window 2>/dev/null | grep -qi "Application Not Responding\|ANR"; then
+ echo "⚠️ ANR dialog still present — force-dismissing with HOME + BACK"
+ adb -s "$DEVICE_ID" shell input keyevent KEYCODE_HOME 2>/dev/null || true
+ sleep 2
+ adb -s "$DEVICE_ID" shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true
+ adb -s "$DEVICE_ID" shell input keyevent KEYCODE_BACK 2>/dev/null || true
+ sleep 1
+ fi
+
+ # Open and close Settings to exercise the system and confirm responsiveness
+ adb -s "$DEVICE_ID" shell am start -a android.settings.SETTINGS 2>/dev/null || true
+ sleep 3
+ adb -s "$DEVICE_ID" shell am force-stop com.android.settings 2>/dev/null || true
+
+ # Final dialog sweep
+ adb -s "$DEVICE_ID" shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS 2>/dev/null || true
+ adb -s "$DEVICE_ID" shell input keyevent KEYCODE_BACK 2>/dev/null || true
+
+ # Clear logcat so agent gets clean logs
+ adb -s "$DEVICE_ID" logcat -c 2>/dev/null || true
+
+ echo "✅ Emulator warmed up and responsive"
+ displayName: 'Warm Up Android Emulator'
+ condition: and(succeeded(), eq('${{ parameters.Platform }}', 'android'))
+ timeoutInMinutes: 3
+
- script: |
echo "Running Copilot PR Reviewer Agent via Review-PR.ps1..."
echo "Reviewing PR #${{ parameters.PRNumber }}..."
+ # Ensure copilot CLI is accessible to pwsh subprocess.
+ # npm global install on Linux goes to UseNode@1 toolcache path which may not
+ # be on PATH inside pwsh even when exported from bash. Create a symlink in
+ # /usr/local/bin which is universally on PATH for all shells.
+ COPILOT_PATH=$(which copilot 2>/dev/null || find /opt/hostedtoolcache/node -name copilot -type f 2>/dev/null | head -1)
+ if [ -n "$COPILOT_PATH" ] && [ ! -f /usr/local/bin/copilot ]; then
+ sudo ln -sf "$COPILOT_PATH" /usr/local/bin/copilot
+ echo "Symlinked copilot to /usr/local/bin/copilot"
+ fi
+ echo "copilot location: $(which copilot 2>/dev/null || echo 'not found')"
+ # Verify pwsh can find it
+ pwsh -NoProfile -c 'Write-Host "pwsh sees copilot at: $(Get-Command copilot -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source)"'
+
# Configure git identity (required for merge operations on self-hosted agents)
git config user.email "copilot-ci@microsoft.com"
git config user.name "Copilot CI"
@@ -636,7 +510,12 @@ stages:
# AcesShared agents may have a newer Xcode than the .NET iOS SDK expects
cp Directory.Build.Override.props.in Directory.Build.Override.props
# Insert ValidateXcodeVersion before closing tag
- sed -i '' 's|| false\n|' Directory.Build.Override.props
+ # GNU sed (Linux) uses -i without suffix; BSD sed (macOS) uses -i ''
+ if [[ "$(uname)" == "Linux" ]]; then
+ sed -i 's|| false\n|' Directory.Build.Override.props
+ else
+ sed -i '' 's|| false\n|' Directory.Build.Override.props
+ fi
# Create artifacts directory for Copilot outputs
mkdir -p $(Build.ArtifactStagingDirectory)/copilot-logs
@@ -645,7 +524,7 @@ stages:
# The script will merge the PR into the current branch
# -PostSummaryComment and -RunFinalize handle posting comments
set +e
- pwsh .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform ${{ parameters.Platform }} -RunFinalize -PostSummaryComment -LogFile "$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md"
+ pwsh -NoProfile .github/scripts/Review-PR.ps1 -PRNumber ${{ parameters.PRNumber }} -Platform ${{ parameters.Platform }} -RunFinalize -PostSummaryComment -LogFile "$(Build.ArtifactStagingDirectory)/copilot-logs/copilot_review_output.md"
COPILOT_EXIT_CODE=$?
set -e
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellBottomNavViewAppearanceTracker.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellBottomNavViewAppearanceTracker.cs
index 1f9d682d41d9..b7718728d990 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellBottomNavViewAppearanceTracker.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellBottomNavViewAppearanceTracker.cs
@@ -131,7 +131,7 @@ static ColorStateList MakeDefaultColorStateList(Context context)
return null;
var baseCSL = AppCompatResources.GetColorStateList(context, mTypedValue.ResourceId);
- var colorPrimary = (ShellRenderer.IsDarkTheme) ? AColor.White : ShellRenderer.DefaultBackgroundColor.ToPlatform();
+ var colorPrimary = (ShellRenderer.IsDarkTheme) ? AColor.White : RuntimeFeature.IsMaterial3Enabled ? Color.FromArgb("#625B71").ToPlatform() : ShellRenderer.DefaultBackgroundColor.ToPlatform();
int defaultColor = baseCSL.DefaultColor;
var disabledcolor = baseCSL.GetColorForState(new[] { -R.Attribute.StateEnabled }, AColor.Gray);
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellPageContainer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellPageContainer.cs
index c58fcf794e9f..408eec70fc87 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellPageContainer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellPageContainer.cs
@@ -14,8 +14,6 @@ internal class ShellPageContainer : ViewGroup
{
static int? DarkBackground;
static int? LightBackground;
-
-
public IViewHandler Child { get; set; }
public bool IsInFragment { get; set; }
@@ -24,13 +22,13 @@ public ShellPageContainer(Context context, IPlatformViewHandler child, bool inFr
{
Child = child;
IsInFragment = inFragment;
- if (child.VirtualView.Background == null)
+ if (child.VirtualView.Background is null)
{
- int color;
- if (ShellRenderer.IsDarkTheme)
- color = DarkBackground ??= ContextCompat.GetColor(context, AColorRes.BackgroundDark);
- else
- color = LightBackground ??= ContextCompat.GetColor(context, AColorRes.BackgroundLight);
+ bool isDark = ShellRenderer.IsDarkTheme;
+
+ int color = RuntimeFeature.IsMaterial3Enabled
+ ? GetMaterial3Background(context)
+ : GetResourceBackground(context, isDark);
child.PlatformView.SetBackgroundColor(new AColor(color));
}
@@ -38,6 +36,27 @@ public ShellPageContainer(Context context, IPlatformViewHandler child, bool inFr
AddView(child.PlatformView);
}
+ int GetMaterial3Background(Context context)
+ {
+ // Material 3 colorSurface automatically adapts to light/dark theme
+ // The theme resolution happens in GetThemeAttrColor based on the active theme
+ return ContextExtensions.GetThemeAttrColor(context, Resource.Attribute.colorSurface);
+ }
+
+ int GetResourceBackground(Context context, bool isDark)
+ {
+ int color;
+ if (isDark)
+ {
+ color = DarkBackground ??= ContextCompat.GetColor(context, AColorRes.BackgroundDark);
+ }
+ else
+ {
+ color = LightBackground ??= ContextCompat.GetColor(context, AColorRes.BackgroundLight);
+ }
+ return color;
+ }
+
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
var width = r - l;
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellRenderer.cs
index 079585280840..e4d614cb7128 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellRenderer.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellRenderer.cs
@@ -92,14 +92,12 @@ void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance)
// These are the primary colors in our styles.xml file
- public static Color DefaultBackgroundColor => ResolveThemeColor(Color.FromArgb("#2c3e50"), Color.FromArgb("#1B3147"));
-
- public static readonly Color DefaultForegroundColor = Colors.White;
- public static readonly Color DefaultTitleColor = Colors.White;
+ public static Color DefaultBackgroundColor => ResolveThemeColor(RuntimeFeature.IsMaterial3Enabled ? Color.FromArgb("#FEF7FF") : Color.FromArgb("#2c3e50"), RuntimeFeature.IsMaterial3Enabled ? Color.FromArgb("#141218") : Color.FromArgb("#1B3147"));
+ public static Color DefaultForegroundColor => ResolveThemeColor(RuntimeFeature.IsMaterial3Enabled ? Color.FromArgb("#1D1B20") : Colors.White, RuntimeFeature.IsMaterial3Enabled ? Color.FromArgb("#E6E0E9") : Colors.White);
+ public static Color DefaultTitleColor => ResolveThemeColor(RuntimeFeature.IsMaterial3Enabled ? Color.FromArgb("#1D1B20") : Colors.White, RuntimeFeature.IsMaterial3Enabled ? Color.FromArgb("#E6E0E9") : Colors.White);
public static readonly Color DefaultUnselectedColor = Color.FromRgba(255, 255, 255, 180);
- internal static Color DefaultBottomNavigationViewBackgroundColor => ResolveThemeColor(Colors.White, Color.FromArgb("#1B3147"));
-
- internal static bool IsDarkTheme => (Application.Current?.RequestedTheme == AppTheme.Dark);
+ internal static Color DefaultBottomNavigationViewBackgroundColor => ResolveThemeColor(RuntimeFeature.IsMaterial3Enabled ? Color.FromArgb("#F3EDF7") : Colors.White, RuntimeFeature.IsMaterial3Enabled ? Color.FromArgb("#1D1B20") : Color.FromArgb("#1B3147"));
+ internal static bool IsDarkTheme => Application.Current?.RequestedTheme == AppTheme.Dark;
static Color ResolveThemeColor(Color light, Color dark)
{
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs
index feda1cdbb481..3d2a6ad03e19 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs
@@ -600,13 +600,22 @@ void UpdateNavBarHasShadow(Page page)
{
if (page == null || !_appBar.IsAlive())
return;
-
if (Shell.GetNavBarHasShadow(page))
{
- if (_appBarElevation <= 0)
- _appBarElevation = _appBar.Context.ToPixels(4);
+ if (RuntimeFeature.IsMaterial3Enabled)
+ {
+ // AppBar elevation is set 0f to match Material 3 AppBar behavior.
+ _appBar.SetElevation(0f);
+ }
+ else
+ {
+ if (_appBarElevation <= 0)
+ {
+ _appBarElevation = _appBar.Context.ToPixels(4);
+ }
- _appBar.SetElevation(_appBarElevation);
+ _appBar.SetElevation(_appBarElevation);
+ }
}
else
{
diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
index cc66326af4d7..6ce1a72fae56 100644
--- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -1,4 +1,8 @@
#nullable enable
+*REMOVED*~static readonly Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.DefaultForegroundColor -> Microsoft.Maui.Graphics.Color
+*REMOVED*~static readonly Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.DefaultTitleColor -> Microsoft.Maui.Graphics.Color
+~static Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.DefaultForegroundColor.get -> Microsoft.Maui.Graphics.Color
+~static Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.DefaultTitleColor.get -> Microsoft.Maui.Graphics.Color
override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void
~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnInterceptTouchEvent(Android.Views.MotionEvent e) -> bool
~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnTouchEvent(Android.Views.MotionEvent e) -> bool
diff --git a/src/Controls/src/Core/TypedBinding.cs b/src/Controls/src/Core/TypedBinding.cs
index 90b1a1d9f30e..4a5ad5545fb2 100644
--- a/src/Controls/src/Core/TypedBinding.cs
+++ b/src/Controls/src/Core/TypedBinding.cs
@@ -147,7 +147,6 @@ public TypedBinding(Func getter, Actio
List> _ancestryChain;
bool _isBindingContextRelativeSource;
BindingMode _cachedMode;
- bool _isSubscribed;
bool _isTSource; // cached type check result
object _cachedDefaultValue; // cached default value
bool _hasDefaultValue;
@@ -289,7 +288,6 @@ internal override void Unapply(bool fromBindingContextChanged = false)
if (_handlers != null)
Unsubscribe();
- _isSubscribed = false;
_cachedMode = BindingMode.Default;
_hasDefaultValue = false;
_cachedDefaultValue = null;
@@ -332,11 +330,12 @@ internal void ApplyCore(object sourceObject, BindableObject target, BindableProp
var needsGetter = (mode == BindingMode.TwoWay && !fromTarget) || mode == BindingMode.OneWay || mode == BindingMode.OneTime;
- // Only subscribe once per binding lifetime
- if (!_isSubscribed && isTSource && (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) && _handlers != null)
+ // Subscribe on every Apply so that intermediate objects that changed are re-subscribed.
+ // Subscribe() is idempotent: it diffs old vs new subscription targets and only
+ // updates what changed, so calling this repeatedly is safe.
+ if (isTSource && (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) && _handlers != null)
{
Subscribe((TSource)sourceObject);
- _isSubscribed = true;
}
if (needsGetter)
diff --git a/src/Controls/tests/Core.UnitTests/TypedBindingUnitTests.cs b/src/Controls/tests/Core.UnitTests/TypedBindingUnitTests.cs
index 5fbf2f11f096..e8ceee33570a 100644
--- a/src/Controls/tests/Core.UnitTests/TypedBindingUnitTests.cs
+++ b/src/Controls/tests/Core.UnitTests/TypedBindingUnitTests.cs
@@ -1773,6 +1773,87 @@ public string Title
}
}
+ [Fact]
+ //https://github.com/dotnet/maui/issues/34428
+ public void TypedBinding_NestedProperty_ResubscribesAfterNullIntermediateBecomesNonNull()
+ {
+ // Regression: when an intermediate object in the path starts as null and later becomes
+ // non-null, the binding must re-establish subscriptions to nested properties.
+ // Previously, the _isSubscribed flag prevented re-subscribing after the first Apply.
+
+ var vm = new ComplexMockViewModel
+ {
+ Model = null // Start with null intermediate
+ };
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), null);
+
+ var binding = new TypedBinding(
+ cvm => cvm.Model is { } m ? (m.Text, true) : (null, false),
+ (cvm, t) => { if (cvm.Model is { } m) m.Text = t; },
+ new[] {
+ new Tuple, string>(cvm => cvm, "Model"),
+ new Tuple, string>(cvm => cvm.Model, "Text")
+ })
+ { Mode = BindingMode.OneWay };
+
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ // Initially null model → binding returns null/default
+ Assert.Null(bindable.GetValue(property));
+
+ // Set Model to non-null → binding should pick up the value
+ vm.Model = new ComplexMockViewModel { Text = "Initial" };
+ Assert.Equal("Initial", (string)bindable.GetValue(property));
+
+ // Change nested property → binding MUST update (this was the regression)
+ vm.Model.Text = "Updated";
+ Assert.Equal("Updated", (string)bindable.GetValue(property));
+ }
+
+ [Fact]
+ //https://github.com/dotnet/maui/issues/34428
+ public void TypedBinding_NestedProperty_ResubscribesAfterIntermediateReplaced()
+ {
+ // When the intermediate object is replaced (non-null → different non-null object),
+ // the binding must switch subscriptions to the new object.
+
+ var child1 = new ComplexMockViewModel { Text = "Child1" };
+ var child2 = new ComplexMockViewModel { Text = "Child2" };
+ var vm = new ComplexMockViewModel { Model = child1 };
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), null);
+
+ var binding = new TypedBinding(
+ cvm => cvm.Model is { } m ? (m.Text, true) : (null, false),
+ (cvm, t) => { if (cvm.Model is { } m) m.Text = t; },
+ new[] {
+ new Tuple, string>(cvm => cvm, "Model"),
+ new Tuple, string>(cvm => cvm.Model, "Text")
+ })
+ { Mode = BindingMode.OneWay };
+
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assert.Equal("Child1", (string)bindable.GetValue(property));
+
+ // Replace intermediate with a different object
+ vm.Model = child2;
+ Assert.Equal("Child2", (string)bindable.GetValue(property));
+
+ // Changing the OLD intermediate should NOT fire the binding
+ child1.Text = "OldChildChanged";
+ Assert.Equal("Child2", (string)bindable.GetValue(property));
+
+ // Changing the NEW intermediate SHOULD fire the binding
+ child2.Text = "Child2Updated";
+ Assert.Equal("Child2Updated", (string)bindable.GetValue(property));
+ }
+
[Fact]
//https://github.com/xamarin/Microsoft.Maui.Controls/issues/3650
//https://github.com/xamarin/Microsoft.Maui.Controls/issues/3613
diff --git a/src/Core/src/Platform/Android/Resources/values/styles-material3.xml b/src/Core/src/Platform/Android/Resources/values/styles-material3.xml
index c5e6cfecd56a..169d12b88745 100644
--- a/src/Core/src/Platform/Android/Resources/values/styles-material3.xml
+++ b/src/Core/src/Platform/Android/Resources/values/styles-material3.xml
@@ -5,6 +5,7 @@
- true
- false
+ - @style/Widget.Material3.BottomNavigationView
- @style/MauiMaterialButton