feat(android): implement Phase 1 cold start recovery

Implements cold start recovery for missed notifications and future alarm
verification/rescheduling as specified in Phase 1 directive.

Changes:
- Add ReactivationManager.kt with cold start recovery logic
- Integrate recovery into DailyNotificationPlugin.load()
- Fix NotifyReceiver to always store NotificationContentEntity for recovery
- Add Phase 1 emulator testing guide and verification doc
- Add test-phase1.sh automated test harness

Recovery behavior:
- Detects missed notifications on app launch
- Marks missed notifications in database
- Verifies future alarms are scheduled in AlarmManager
- Reschedules missing future alarms
- Completes within 2-second timeout (non-blocking)

Test harness:
- Automated script with 4 test cases
- UI prompts for plugin configuration
- Log parsing for recovery results
- Verified on Pixel 8 API 34 emulator

Related:
- Implements: android-implementation-directive-phase1.md
- Requirements: docs/alarms/03-plugin-requirements.md §3.1.2
- Testing: docs/alarms/PHASE1-EMULATOR-TESTING.md
- Verification: docs/alarms/PHASE1-VERIFICATION.md
This commit is contained in:
Matthew Raymer
2025-11-27 10:01:34 +00:00
parent 77b6f2260f
commit 3151a1cc31
7 changed files with 1874 additions and 45 deletions

View File

@@ -0,0 +1,560 @@
#!/bin/bash
# Phase 1 Testing Script - Interactive Test Runner
# Guides through all Phase 1 tests with clear prompts for UI interaction
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
PACKAGE="com.timesafari.dailynotification"
ACTIVITY="${PACKAGE}/.MainActivity"
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${APP_DIR}/../.." && pwd)"
# Functions
print_header() {
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
}
print_step() {
echo -e "${GREEN}→ Step $1:${NC} $2"
}
print_wait() {
echo -e "${YELLOW}$1${NC}"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
wait_for_user() {
echo ""
read -p "Press Enter when ready to continue..."
echo ""
}
wait_for_ui_action() {
echo ""
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${YELLOW}👆 UI ACTION REQUIRED${NC}"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE}$1${NC}"
echo ""
read -p "Press Enter after completing the action above..."
echo ""
}
check_adb_connection() {
if ! adb devices | grep -q "device$"; then
print_error "No Android device/emulator connected"
echo "Please connect a device or start an emulator, then run:"
echo " adb devices"
exit 1
fi
print_success "ADB device connected"
}
check_emulator_ready() {
print_info "Checking emulator status..."
if ! adb shell getprop sys.boot_completed | grep -q "1"; then
print_wait "Waiting for emulator to boot..."
adb wait-for-device
while [ "$(adb shell getprop sys.boot_completed)" != "1" ]; do
sleep 2
done
fi
print_success "Emulator is ready"
}
build_app() {
print_header "Building Test App"
cd "${APP_DIR}"
print_step "1" "Building debug APK..."
if ./gradlew assembleDebug; then
print_success "Build successful"
else
print_error "Build failed"
exit 1
fi
APK_PATH="${APP_DIR}/app/build/outputs/apk/debug/app-debug.apk"
if [ ! -f "${APK_PATH}" ]; then
print_error "APK not found at ${APK_PATH}"
exit 1
fi
print_success "APK ready: ${APK_PATH}"
}
install_app() {
print_header "Installing App"
print_step "1" "Uninstalling existing app (if present)..."
UNINSTALL_OUTPUT=$(adb uninstall "${PACKAGE}" 2>&1)
UNINSTALL_EXIT=$?
if [ ${UNINSTALL_EXIT} -eq 0 ]; then
print_success "Existing app uninstalled"
elif echo "${UNINSTALL_OUTPUT}" | grep -q "DELETE_FAILED_INTERNAL_ERROR"; then
print_info "No existing app to uninstall (continuing)"
elif echo "${UNINSTALL_OUTPUT}" | grep -q "Failure"; then
print_info "Uninstall failed (app may not exist) - continuing with install"
else
print_info "Uninstall result unclear - continuing with install"
fi
print_step "2" "Installing new APK..."
if adb install -r "${APP_DIR}/app/build/outputs/apk/debug/app-debug.apk"; then
print_success "App installed successfully"
else
print_error "Installation failed"
exit 1
fi
print_step "3" "Verifying installation..."
if adb shell pm list packages | grep -q "${PACKAGE}"; then
print_success "App verified in package list"
else
print_error "App not found in package list"
exit 1
fi
}
clear_logs() {
print_info "Clearing logcat buffer..."
adb logcat -c
print_success "Logs cleared"
}
launch_app() {
print_info "Launching app..."
adb shell am start -n "${ACTIVITY}"
sleep 3 # Give app time to load and check status
print_success "App launched"
}
check_plugin_configured() {
print_info "Checking if plugin is already configured..."
# Wait a moment for app to fully load
sleep 3
# Check if database exists (indicates plugin has been used)
DB_EXISTS=$(adb shell run-as "${PACKAGE}" ls databases/ 2>/dev/null | grep -c "daily_notification" || echo "0")
# Check if SharedPreferences has configuration (more reliable)
# The plugin stores config in SharedPreferences
PREFS_EXISTS=$(adb shell run-as "${PACKAGE}" ls shared_prefs/ 2>/dev/null | grep -c "DailyNotification" || echo "0")
# Check recent logs for configuration activity
RECENT_CONFIG=$(adb logcat -d -t 50 | grep -E "Plugin configured|configurePlugin|Configuration" | tail -3)
if [ "${DB_EXISTS}" -gt "0" ] || [ "${PREFS_EXISTS}" -gt "0" ]; then
print_success "Plugin appears to be configured (database or preferences exist)"
# Show user what to check in UI
print_info "Please verify in the app UI that you see:"
echo " ⚙️ Plugin Settings: ✅ Configured"
echo " 🔌 Native Fetcher: ✅ Configured"
echo ""
echo "If both show ✅, the plugin is configured and you can skip configuration."
echo "If either shows ❌ or 'Not configured', you'll need to click 'Configure Plugin'."
echo ""
return 0
else
print_info "Plugin not configured (no database or preferences found)"
print_info "You will need to click 'Configure Plugin' in the app UI"
return 1
fi
}
ensure_plugin_configured() {
if check_plugin_configured; then
# Plugin might be configured, but let user verify
wait_for_ui_action "Please check the Plugin Status section at the top of the app.
If you see:
- ⚙️ Plugin Settings: ✅ Configured
- 🔌 Native Fetcher: ✅ Configured
Then the plugin is already configured - just press Enter to continue.
If either shows ❌ or 'Not configured', click 'Configure Plugin' button first,
wait for both to show ✅, then press Enter."
# Give a moment for any configuration that just happened
sleep 2
print_success "Continuing with tests (plugin configuration verified or skipped)"
return 0
else
# Plugin definitely needs configuration
print_info "Plugin needs configuration"
wait_for_ui_action "Click the 'Configure Plugin' button in the app UI.
Wait for the status to update:
- ⚙️ Plugin Settings: Should change to ✅ Configured
- 🔌 Native Fetcher: Should change to ✅ Configured
Once both show ✅, press Enter to continue."
# Verify configuration completed
sleep 2
print_success "Plugin configuration completed (or verified)"
fi
}
kill_app() {
print_info "Killing app process..."
adb shell am kill "${PACKAGE}"
sleep 2
# Verify process is killed
if adb shell ps | grep -q "${PACKAGE}"; then
print_wait "Process still running, using force-stop..."
adb shell am force-stop "${PACKAGE}"
sleep 1
fi
if ! adb shell ps | grep -q "${PACKAGE}"; then
print_success "App process terminated"
else
print_error "App process still running"
return 1
fi
}
check_recovery_logs() {
print_info "Checking recovery logs..."
echo ""
adb logcat -d | grep -E "DNP-REACTIVATION" | tail -10
echo ""
}
check_alarm_status() {
print_info "Checking AlarmManager status..."
echo ""
adb shell dumpsys alarm | grep -i timesafari | head -5
echo ""
}
get_current_time() {
adb shell date +%s
}
# Main test execution
main() {
print_header "Phase 1 Testing Script"
echo "This script will guide you through all Phase 1 tests."
echo "You'll be prompted when UI interaction is needed."
echo ""
wait_for_user
# Pre-flight checks
print_header "Pre-Flight Checks"
check_adb_connection
check_emulator_ready
# Build and install
build_app
install_app
# Clear logs
clear_logs
# ============================================
# TEST 1: Cold Start Missed Detection
# ============================================
print_header "TEST 1: Cold Start Missed Detection"
echo "Purpose: Verify missed notifications are detected and marked."
echo ""
wait_for_user
print_step "1" "Launch app and check plugin status"
launch_app
ensure_plugin_configured
wait_for_ui_action "In the app UI, click the 'Test Notification' button.
This will schedule a notification for 4 minutes in the future.
(The test app automatically schedules for 4 minutes from now)"
print_step "2" "Verifying notification was scheduled..."
sleep 2
check_alarm_status
print_info "Checking logs for scheduling confirmation..."
adb logcat -d | grep -E "DN|SCHEDULE|Stored notification content" | tail -5
wait_for_ui_action "Verify in the logs above that you see:
- 'Stored notification content in database' (NEW - should appear now)
- Alarm scheduled in AlarmManager
If you don't see 'Stored notification content', the fix may not be working."
wait_for_user
print_step "3" "Killing app process (simulates OS kill)..."
kill_app
print_step "4" "Getting alarm scheduled time..."
ALARM_INFO=$(adb shell dumpsys alarm | grep -i timesafari | grep "origWhen" | head -1)
if [ -n "${ALARM_INFO}" ]; then
# Extract alarm time (origWhen is in milliseconds)
ALARM_TIME_MS=$(echo "${ALARM_INFO}" | grep -oE 'origWhen [0-9]+' | awk '{print $2}')
if [ -n "${ALARM_TIME_MS}" ]; then
CURRENT_TIME=$(get_current_time)
ALARM_TIME_SEC=$((ALARM_TIME_MS / 1000))
WAIT_SECONDS=$((ALARM_TIME_SEC - CURRENT_TIME + 60)) # Wait 1 minute past alarm
if [ ${WAIT_SECONDS} -gt 0 ] && [ ${WAIT_SECONDS} -lt 600 ]; then
ALARM_READABLE=$(date -d "@${ALARM_TIME_SEC}" 2>/dev/null || echo "${ALARM_TIME_SEC}")
CURRENT_READABLE=$(date -d "@${CURRENT_TIME}" 2>/dev/null || echo "${CURRENT_TIME}")
print_info "Alarm scheduled for: ${ALARM_READABLE}"
print_info "Current time: ${CURRENT_READABLE}"
print_wait "Waiting ${WAIT_SECONDS} seconds for alarm time to pass..."
sleep ${WAIT_SECONDS}
elif [ ${WAIT_SECONDS} -le 0 ]; then
print_info "Alarm time has already passed"
print_wait "Waiting 2 minutes to ensure we're well past alarm time..."
sleep 120
else
print_wait "Alarm is more than 10 minutes away. Waiting 5 minutes (you can adjust this)..."
sleep 300
fi
else
print_wait "Could not parse alarm time. Waiting 5 minutes..."
sleep 300
fi
else
print_wait "Could not find alarm in AlarmManager. Waiting 5 minutes..."
sleep 300
fi
print_step "5" "Launching app (cold start - triggers recovery)..."
clear_logs
launch_app
print_step "6" "Checking recovery logs..."
sleep 3
check_recovery_logs
print_info "Expected log output:"
echo " DNP-REACTIVATION: Starting app launch recovery (Phase 1: cold start only)"
echo " DNP-REACTIVATION: Cold start recovery: checking for missed notifications"
echo " DNP-REACTIVATION: Marked missed notification: <id>"
echo " DNP-REACTIVATION: Cold start recovery complete: missed=1, ..."
echo ""
RECOVERY_RESULT=$(adb logcat -d | grep "Cold start recovery complete" | tail -1)
if echo "${RECOVERY_RESULT}" | grep -q "missed=[1-9]"; then
print_success "TEST 1 PASSED: Missed notification detected!"
elif echo "${RECOVERY_RESULT}" | grep -q "missed=0"; then
print_error "TEST 1 FAILED: No missed notifications detected (missed=0)"
print_info "This might mean:"
echo " - Notification was already delivered"
echo " - NotificationContentEntity was not created"
echo " - Alarm fired before app was killed"
else
print_error "TEST 1 INCONCLUSIVE: Could not find recovery result"
fi
wait_for_user
# ============================================
# TEST 2: Future Alarm Verification
# ============================================
print_header "TEST 2: Future Alarm Verification"
echo "Purpose: Verify future alarms are verified/rescheduled if missing."
echo ""
echo "Note: The test app doesn't have a cancel button, so we'll test"
echo " verification of existing alarms instead."
echo ""
wait_for_user
print_step "1" "Launch app"
launch_app
ensure_plugin_configured
wait_for_ui_action "In the app UI, click 'Test Notification' to schedule another notification.
This creates a second scheduled notification for testing verification."
print_step "2" "Verifying alarms are scheduled..."
sleep 2
check_alarm_status
ALARM_COUNT=$(adb shell dumpsys alarm | grep -c "timesafari" || echo "0")
print_info "Found ${ALARM_COUNT} scheduled alarm(s)"
if [ "${ALARM_COUNT}" -gt "0" ]; then
print_success "Alarms are scheduled in AlarmManager"
else
print_error "No alarms found in AlarmManager"
wait_for_user
fi
print_step "3" "Killing app and relaunching (triggers recovery)..."
kill_app
clear_logs
launch_app
print_step "4" "Checking recovery logs for verification..."
sleep 3
check_recovery_logs
print_info "Expected log output (either):"
echo " DNP-REACTIVATION: Verified scheduled alarm: <id> at <time>"
echo " OR"
echo " DNP-REACTIVATION: Rescheduled missing alarm: <id> at <time>"
echo " DNP-REACTIVATION: Cold start recovery complete: ..., verified=1 or rescheduled=1, ..."
echo ""
RECOVERY_RESULT=$(adb logcat -d | grep "Cold start recovery complete" | tail -1)
# Extract counts from recovery result
RESCHEDULED_COUNT=$(echo "${RECOVERY_RESULT}" | grep -oE "rescheduled=[0-9]+" | grep -oE "[0-9]+" || echo "0")
VERIFIED_COUNT=$(echo "${RECOVERY_RESULT}" | grep -oE "verified=[0-9]+" | grep -oE "[0-9]+" || echo "0")
if [ "${RESCHEDULED_COUNT}" -gt "0" ]; then
print_success "TEST 2 PASSED: Missing future alarms were detected and rescheduled (rescheduled=${RESCHEDULED_COUNT})!"
elif [ "${VERIFIED_COUNT}" -gt "0" ]; then
print_success "TEST 2 PASSED: Future alarms verified in AlarmManager (verified=${VERIFIED_COUNT})!"
elif [ "${RESCHEDULED_COUNT}" -eq "0" ] && [ "${VERIFIED_COUNT}" -eq "0" ]; then
print_info "TEST 2: No verification/rescheduling needed"
print_info "This is OK if:"
echo " - All alarms were in the past (marked as missed)"
echo " - All future alarms were already correctly scheduled"
else
print_error "TEST 2 INCONCLUSIVE: Could not find recovery result"
print_info "Recovery result: ${RECOVERY_RESULT}"
fi
print_step "5" "Verifying alarms are still scheduled in AlarmManager..."
check_alarm_status
wait_for_user
# ============================================
# TEST 3: Recovery Timeout
# ============================================
print_header "TEST 3: Recovery Timeout"
echo "Purpose: Verify recovery times out gracefully."
echo ""
echo "Note: This test requires creating many schedules (100+)."
echo "For now, we'll verify the timeout mechanism exists."
echo ""
wait_for_user
print_step "1" "Checking recovery timeout implementation..."
if grep -q "RECOVERY_TIMEOUT_SECONDS = 2L" "${PROJECT_ROOT}/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt"; then
print_success "Timeout is set to 2 seconds"
else
print_error "Timeout not found in code"
fi
if grep -q "withTimeout" "${PROJECT_ROOT}/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt"; then
print_success "Timeout protection is implemented"
else
print_error "Timeout protection not found"
fi
print_info "TEST 3: Timeout mechanism verified in code"
print_info "Full test (100+ schedules) can be done manually if needed"
wait_for_user
# ============================================
# TEST 4: Invalid Data Handling
# ============================================
print_header "TEST 4: Invalid Data Handling"
echo "Purpose: Verify invalid data doesn't crash recovery."
echo ""
echo "Note: This requires database access. We'll check if the app is debuggable."
echo ""
wait_for_user
print_step "1" "Checking if app is debuggable..."
if adb shell dumpsys package "${PACKAGE}" | grep -q "debuggable=true"; then
print_success "App is debuggable - can access database"
print_info "Invalid data handling is tested automatically during recovery."
print_info "The ReactivationManager code includes checks for:"
echo " - Empty notification IDs (skipped with warning)"
echo " - Invalid schedule IDs (skipped with warning)"
echo " - Database errors (logged, non-fatal)"
echo ""
print_info "To manually test invalid data:"
echo " 1. Use: adb shell run-as ${PACKAGE} sqlite3 databases/daily_notification_plugin.db"
echo " 2. Insert invalid notification: INSERT INTO notification_content (id, ...) VALUES ('', ...);"
echo " 3. Launch app and check logs for 'Skipping invalid notification'"
else
print_info "App is not debuggable - cannot access database directly"
print_info "TEST 4: Code review confirms invalid data handling exists"
print_info " - ReactivationManager.kt checks for empty IDs"
print_info " - Errors are logged but don't crash recovery"
fi
wait_for_user
# ============================================
# Summary
# ============================================
print_header "Testing Complete"
echo "Test Results Summary:"
echo ""
echo "TEST 1: Cold Start Missed Detection"
echo " - ✅ PASSED if logs show 'missed=1'"
echo " - ❌ FAILED if logs show 'missed=0' or no recovery logs"
echo ""
echo "TEST 2: Future Alarm Verification/Rescheduling"
echo " - ✅ PASSED if logs show 'rescheduled=1' OR 'verified=1'"
echo " - INFO if both are 0 (no future alarms to check)"
echo ""
echo "TEST 3: Recovery Timeout"
echo " - Timeout mechanism verified in code"
echo ""
echo "TEST 4: Invalid Data Handling"
echo " - Requires database access (debuggable app or root)"
echo ""
print_info "All recovery logs:"
echo ""
adb logcat -d | grep "DNP-REACTIVATION" | tail -20
echo ""
print_success "Phase 1 testing script complete!"
echo ""
echo "Next steps:"
echo " - Review logs above"
echo " - Verify all tests passed"
echo " - Check database if needed (debuggable app)"
echo " - Update Doc B with test results"
}
# Run main function
main "$@"