2 Commits

Author SHA1 Message Date
Matthew
4e25841fe9 fix(test-app): auto-detect Android SDK and allow build without adb
Previously, the build script would skip Android builds entirely if
adb was not in PATH, even though adb is only needed for installing/
launching apps, not for building APKs.

Changes:
- Added find_android_sdk() function that automatically detects SDK
  location via ANDROID_HOME, ANDROID_SDK_ROOT, existing local.properties,
  or common default locations (macOS/Linux)
- Automatically creates/updates android/local.properties with detected
  SDK location
- Removed early exit when adb not found - build now proceeds without adb
- Moved adb check to only when installing/launching apps (--run flags)
- Updated warning messages to clarify adb is only needed for install/launch

This allows developers to build APKs even when Android SDK platform-tools
are not in PATH, improving build script usability.
2026-02-03 00:34:25 -08:00
Matthew
367325452a fix(android): explicitly set component and package for AlarmManager broadcasts
AlarmManager was firing alarms but DailyNotificationReceiver was not
receiving broadcasts. The issue was that Intents created with
Intent(context, Class) constructor were not reliably matched by
AlarmManager when delivering broadcasts.

Solution: Explicitly set ComponentName and package on all Intents used
for AlarmManager broadcasts. This ensures AlarmManager can correctly
match PendingIntents to the registered receiver.

Changes:
- NotifyReceiver.kt: Fixed Intent creation in scheduleNotification(),
  cancelNotification(), isAlarmScheduled(), and idempotence checks
- ReactivationManager.kt: Fixed alarmsExist() to use
  DailyNotificationReceiver with explicit component/package
- DailyNotificationScheduler.java: Fixed Intent creation to explicitly
  set component and package

This fixes the critical bug where alarms fire but receivers are not
triggered, resolving the gap between AlarmManager delivery and receiver
execution.
2026-02-03 00:33:33 -08:00
4 changed files with 163 additions and 43 deletions

View File

@@ -12,6 +12,7 @@ package com.timesafari.dailynotification;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
@@ -154,9 +155,15 @@ public class DailyNotificationScheduler {
cancelNotification(duplicateId); cancelNotification(duplicateId);
} }
// Create intent for the notification // CRITICAL FIX: Explicitly set component and package for AlarmManager broadcasts
Intent intent = new Intent(context, DailyNotificationReceiver.class); // AlarmManager requires explicit component matching when delivering broadcasts
intent.setAction(com.timesafari.dailynotification.DailyNotificationConstants.ACTION_NOTIFICATION); ComponentName receiverComponent = new ComponentName(
context.getPackageName(),
"com.timesafari.dailynotification.DailyNotificationReceiver"
);
Intent intent = new Intent(com.timesafari.dailynotification.DailyNotificationConstants.ACTION_NOTIFICATION);
intent.setComponent(receiverComponent);
intent.setPackage(context.getPackageName());
intent.putExtra(com.timesafari.dailynotification.DailyNotificationConstants.EXTRA_NOTIFICATION_ID, content.getId()); intent.putExtra(com.timesafari.dailynotification.DailyNotificationConstants.EXTRA_NOTIFICATION_ID, content.getId());
// Check if this is a static reminder // Check if this is a static reminder

View File

@@ -6,6 +6,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
@@ -146,8 +147,15 @@ class NotifyReceiver : BroadcastReceiver() {
// This prevents duplicate alarms when multiple scheduling paths race // This prevents duplicate alarms when multiple scheduling paths race
// Strategy: Check both by scheduleId (stable) and by trigger time (catches different scheduleIds for same time) // Strategy: Check both by scheduleId (stable) and by trigger time (catches different scheduleIds for same time)
val requestCode = getRequestCode(stableScheduleId) val requestCode = getRequestCode(stableScheduleId)
val checkIntent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { // CRITICAL FIX: Explicitly set component and package for AlarmManager broadcasts
action = "com.timesafari.daily.NOTIFICATION" // AlarmManager requires explicit component matching when delivering broadcasts
val receiverComponent = ComponentName(
context.packageName,
"com.timesafari.dailynotification.DailyNotificationReceiver"
)
val checkIntent = Intent("com.timesafari.daily.NOTIFICATION").apply {
setComponent(receiverComponent)
setPackage(context.packageName)
} }
// Check 1: Same scheduleId (stable requestCode) - most reliable // Check 1: Same scheduleId (stable requestCode) - most reliable
@@ -269,12 +277,21 @@ class NotifyReceiver : BroadcastReceiver() {
Log.w(TAG, "Failed to store notification content in database, continuing with alarm scheduling", e) Log.w(TAG, "Failed to store notification content in database, continuing with alarm scheduling", e)
} }
// FIX: Use DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver // CRITICAL FIX: Explicitly set component and package for AlarmManager broadcasts
// FIX: Set action to match manifest registration // AlarmManager requires explicit component matching when delivering broadcasts.
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { // Using Intent(context, Class) constructor may not work reliably with AlarmManager
action = "com.timesafari.daily.NOTIFICATION" // Must match manifest intent-filter action // on all Android versions, especially when the app is in certain states.
putExtra("notification_id", notificationId) // DailyNotificationReceiver expects this extra // Solution: Create Intent with action, then explicitly set component and package.
putExtra("schedule_id", stableScheduleId) // Add stable scheduleId for tracking val intent = Intent("com.timesafari.daily.NOTIFICATION").apply {
// Explicitly set component to ensure AlarmManager can match it to the receiver
setComponent(receiverComponent)
// Explicitly set package to ensure it matches the app's package (not plugin's)
setPackage(context.packageName)
// Must match manifest intent-filter action
// DailyNotificationReceiver expects this extra
putExtra("notification_id", notificationId)
// Add stable scheduleId for tracking
putExtra("schedule_id", stableScheduleId)
// Also preserve original extras for backward compatibility if needed // Also preserve original extras for backward compatibility if needed
putExtra("title", config.title) putExtra("title", config.title)
putExtra("body", config.body) putExtra("body", config.body)
@@ -282,7 +299,8 @@ class NotifyReceiver : BroadcastReceiver() {
putExtra("vibration", config.vibration ?: true) putExtra("vibration", config.vibration ?: true)
putExtra("priority", config.priority ?: "normal") putExtra("priority", config.priority ?: "normal")
putExtra("is_static_reminder", isStaticReminder) putExtra("is_static_reminder", isStaticReminder)
putExtra("trigger_time", triggerAtMillis) // Store trigger time for debugging // Store trigger time for debugging
putExtra("trigger_time", triggerAtMillis)
if (reminderId != null) { if (reminderId != null) {
putExtra("reminder_id", reminderId) putExtra("reminder_id", reminderId)
} }
@@ -407,9 +425,14 @@ class NotifyReceiver : BroadcastReceiver() {
*/ */
fun cancelNotification(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null) { fun cancelNotification(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
// FIX: Use DailyNotificationReceiver to match what was scheduled // CRITICAL FIX: Use same Intent format as scheduling (explicit component and package)
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { val receiverComponent = ComponentName(
action = "com.timesafari.daily.NOTIFICATION" context.packageName,
"com.timesafari.dailynotification.DailyNotificationReceiver"
)
val intent = Intent("com.timesafari.daily.NOTIFICATION").apply {
setComponent(receiverComponent)
setPackage(context.packageName)
} }
val requestCode = when { val requestCode = when {
scheduleId != null -> getRequestCode(scheduleId) scheduleId != null -> getRequestCode(scheduleId)
@@ -438,9 +461,14 @@ class NotifyReceiver : BroadcastReceiver() {
* @return true if alarm is scheduled, false otherwise * @return true if alarm is scheduled, false otherwise
*/ */
fun isAlarmScheduled(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null): Boolean { fun isAlarmScheduled(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null): Boolean {
// FIX: Use DailyNotificationReceiver to match what was scheduled // CRITICAL FIX: Use same Intent format as scheduling (explicit component and package)
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { val receiverComponent = ComponentName(
action = "com.timesafari.daily.NOTIFICATION" context.packageName,
"com.timesafari.dailynotification.DailyNotificationReceiver"
)
val intent = Intent("com.timesafari.daily.NOTIFICATION").apply {
setComponent(receiverComponent)
setPackage(context.packageName)
} }
val requestCode = when { val requestCode = when {
scheduleId != null -> getRequestCode(scheduleId) scheduleId != null -> getRequestCode(scheduleId)

View File

@@ -1,6 +1,7 @@
package com.timesafari.dailynotification package com.timesafari.dailynotification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
@@ -442,8 +443,14 @@ class ReactivationManager(private val context: Context) {
return try { return try {
// Check if any PendingIntent for our receiver exists // Check if any PendingIntent for our receiver exists
// This is more reliable than nextAlarmClock // This is more reliable than nextAlarmClock
val intent = Intent(context, NotifyReceiver::class.java).apply { // CRITICAL FIX: Use DailyNotificationReceiver with explicit component/package
action = "com.timesafari.daily.NOTIFICATION" val receiverComponent = ComponentName(
context.packageName,
"com.timesafari.dailynotification.DailyNotificationReceiver"
)
val intent = Intent("com.timesafari.daily.NOTIFICATION").apply {
setComponent(receiverComponent)
setPackage(context.packageName)
} }
val pendingIntent = PendingIntent.getBroadcast( val pendingIntent = PendingIntent.getBroadcast(
context, context,

View File

@@ -108,7 +108,8 @@ check_requirements() {
# Check Android requirements if building Android # Check Android requirements if building Android
if [ "$BUILD_ALL" = true ] || [ "$BUILD_ANDROID" = true ]; then if [ "$BUILD_ALL" = true ] || [ "$BUILD_ANDROID" = true ]; then
if ! command -v adb &> /dev/null; then if ! command -v adb &> /dev/null; then
log_warn "Android SDK not found (adb not in PATH). Android build will be skipped." log_warn "Android SDK tools not found (adb not in PATH)."
log_warn "APK can still be built, but install/launch requires adb."
else else
log_info "✅ Android SDK: $(adb version | head -1)" log_info "✅ Android SDK: $(adb version | head -1)"
fi fi
@@ -238,18 +239,88 @@ if [ "$BUILD_ALL" = true ] || [ "$BUILD_IOS" = true ]; then
fi fi
fi fi
# Find Android SDK location
find_android_sdk() {
local android_dir=""
local local_props="$PROJECT_DIR/android/local.properties"
# Check environment variables first
if [ -n "$ANDROID_HOME" ] && [ -d "$ANDROID_HOME" ]; then
android_dir="$ANDROID_HOME"
log_info "Found Android SDK via ANDROID_HOME: $android_dir"
elif [ -n "$ANDROID_SDK_ROOT" ] && [ -d "$ANDROID_SDK_ROOT" ]; then
android_dir="$ANDROID_SDK_ROOT"
log_info "Found Android SDK via ANDROID_SDK_ROOT: $android_dir"
fi
# Check existing local.properties
if [ -z "$android_dir" ] && [ -f "$local_props" ]; then
# Temporarily disable exit on error for grep (may not find match)
set +e
sdk_line=$(grep "^sdk.dir=" "$local_props" 2>/dev/null)
set -e
if [ -n "$sdk_line" ]; then
android_dir=$(echo "$sdk_line" | cut -d'=' -f2 | sed 's|\\\\|/|g' | sed "s|^~|$HOME|")
if [ -n "$android_dir" ] && [ -d "$android_dir" ]; then
log_info "Found Android SDK in local.properties: $android_dir"
else
android_dir=""
fi
fi
fi
# Try common locations
if [ -z "$android_dir" ]; then
# macOS default location
if [ -d "$HOME/Library/Android/sdk" ]; then
android_dir="$HOME/Library/Android/sdk"
log_info "Found Android SDK in default macOS location: $android_dir"
# Linux default location
elif [ -d "$HOME/Android/Sdk" ]; then
android_dir="$HOME/Android/Sdk"
log_info "Found Android SDK in default Linux location: $android_dir"
fi
fi
# Create/update local.properties if SDK found
if [ -n "$android_dir" ]; then
# Normalize path (convert to forward slashes, expand ~)
android_dir=$(echo "$android_dir" | sed 's|\\\\|/|g' | sed "s|^~|$HOME|")
# Create local.properties with SDK location
mkdir -p "$(dirname "$local_props")"
echo "## This file is automatically generated by build script" > "$local_props"
echo "## Location: $android_dir" >> "$local_props"
echo "sdk.dir=$android_dir" >> "$local_props"
log_info "✅ Configured Android SDK in local.properties"
return 0
else
log_error "Android SDK not found!"
log_error "Please set one of the following:"
log_error " 1. ANDROID_HOME environment variable"
log_error " 2. ANDROID_SDK_ROOT environment variable"
log_error " 3. Create android/local.properties with: sdk.dir=/path/to/android/sdk"
log_error ""
log_error "Common SDK locations:"
log_error " macOS: ~/Library/Android/sdk"
log_error " Linux: ~/Android/Sdk"
return 1
fi
}
# Android build # Android build
if [ "$BUILD_ALL" = true ] || [ "$BUILD_ANDROID" = true ]; then if [ "$BUILD_ALL" = true ] || [ "$BUILD_ANDROID" = true ]; then
log_step "Building Android app..." log_step "Building Android app..."
# Check for Android SDK # Ensure Android SDK is configured
if ! command -v adb &> /dev/null; then if ! find_android_sdk; then
log_warn "adb not found. Android SDK may not be installed." log_error "Cannot build Android app without SDK location"
log_warn "Skipping Android build. Install Android SDK to build Android." exit 1
else fi
cd "$PROJECT_DIR/android" cd "$PROJECT_DIR/android"
# Build APK # Build APK (Gradle doesn't require adb for building)
if ./gradlew :app:assembleDebug; then if ./gradlew :app:assembleDebug; then
log_info "Android APK built successfully" log_info "Android APK built successfully"
@@ -258,8 +329,15 @@ if [ "$BUILD_ALL" = true ] || [ "$BUILD_ANDROID" = true ]; then
if [ -f "$APK_PATH" ]; then if [ -f "$APK_PATH" ]; then
log_info "APK location: $APK_PATH" log_info "APK location: $APK_PATH"
# Run on emulator if requested # Run on emulator if requested (requires adb)
if [ "$RUN_ALL" = true ] || [ "$RUN_ANDROID" = true ]; then if [ "$RUN_ALL" = true ] || [ "$RUN_ANDROID" = true ]; then
# Check for Android SDK tools (adb)
if ! command -v adb &> /dev/null; then
log_warn "adb not found in PATH. Cannot install/launch app."
log_warn "APK built successfully, but install/launch requires Android SDK."
log_info "To install manually: adb install -r $APK_PATH"
log_info "Or add Android SDK platform-tools to your PATH."
else
log_step "Installing and launching Android app..." log_step "Installing and launching Android app..."
# Check for running emulator # Check for running emulator
@@ -283,6 +361,7 @@ if [ "$BUILD_ALL" = true ] || [ "$BUILD_ANDROID" = true ]; then
fi fi
fi fi
fi fi
fi
else else
log_error "APK not found at expected location: $APK_PATH" log_error "APK not found at expected location: $APK_PATH"
fi fi
@@ -293,7 +372,6 @@ if [ "$BUILD_ALL" = true ] || [ "$BUILD_ANDROID" = true ]; then
cd "$PROJECT_DIR" cd "$PROJECT_DIR"
fi fi
fi
# iOS build # iOS build
if [ "$BUILD_ALL" = true ] || [ "$BUILD_IOS" = true ]; then if [ "$BUILD_ALL" = true ] || [ "$BUILD_IOS" = true ]; then