Files
daily-notification-plugin/test-apps/android-test-app/test-phase1.sh
Matthew Raymer 3151a1cc31 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
2025-11-27 10:01:34 +00:00

561 lines
19 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "$@"