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
6 changed files with 201 additions and 176 deletions

View File

@@ -44,11 +44,9 @@ npx cap run android
## Prerequisites ## Prerequisites
### Required Software ### Required Software
- **Android Studio** (latest stable version) - for Android development - **Android Studio** (latest stable version)
- **Java 11+** (for Kotlin compilation) - **Java 11+** (for Kotlin compilation)
- **Android SDK** with API level 21+ - **Android SDK** with API level 21+
- **Xcode** (latest stable version) - for iOS development (macOS only)
- **Xcode Command Line Tools** - required for iOS builds (includes `xcodebuild`, `sqlite3`, etc.)
- **Node.js** 16+ (for TypeScript compilation) - **Node.js** 16+ (for TypeScript compilation)
- **npm** or **yarn** (for dependency management) - **npm** or **yarn** (for dependency management)
@@ -56,35 +54,11 @@ npx cap run android
- **Gradle Wrapper** (included in project) - **Gradle Wrapper** (included in project)
- **Kotlin** (configured in build.gradle) - **Kotlin** (configured in build.gradle)
- **TypeScript** (for plugin interface) - **TypeScript** (for plugin interface)
- **CocoaPods** - for iOS dependency management
### iOS-Specific Prerequisites
**Xcode Command Line Tools** are required for iOS builds. The build script will verify these are installed:
```bash
# Install Xcode Command Line Tools (if not already installed)
xcode-select --install
```
**Verification:**
```bash
# Check if Command Line Tools are configured
xcode-select -p
# Verify xcodebuild is available
xcodebuild -version
# Verify sqlite3 is available (part of Command Line Tools)
sqlite3 --version
```
**Note:** The build script automatically checks for Command Line Tools and will fail with clear error messages if they're missing.
### System Requirements ### System Requirements
- **RAM**: 4GB minimum, 8GB recommended - **RAM**: 4GB minimum, 8GB recommended
- **Storage**: 2GB free space - **Storage**: 2GB free space
- **OS**: Windows 10+, macOS 10.14+, or Linux (iOS development requires macOS) - **OS**: Windows 10+, macOS 10.14+, or Linux
## Build Methods ## Build Methods
@@ -323,8 +297,6 @@ android/build/reports/tests/test/index.html
### iOS Native Build Process ### iOS Native Build Process
**Prerequisites:** Ensure Xcode Command Line Tools are installed (see [Prerequisites](#prerequisites) section). The build script will verify this automatically.
#### 1. Navigate to iOS Directory #### 1. Navigate to iOS Directory
```bash ```bash
cd ios cd ios
@@ -335,12 +307,6 @@ cd ios
pod install pod install
``` ```
**Note:** If you encounter issues with `pod install`, ensure Xcode Command Line Tools are properly configured:
```bash
xcode-select --install # Install if missing
xcode-select -p # Verify installation path
```
#### 3. Build Commands #### 3. Build Commands
```bash ```bash
# Build using Xcode command line # Build using Xcode command line
@@ -816,13 +782,6 @@ The project includes several automated build scripts in the `scripts/` directory
./scripts/build-native.sh --platform ios ./scripts/build-native.sh --platform ios
./scripts/build-native.sh --verbose ./scripts/build-native.sh --verbose
# Clean build (removes all build artifacts and caches)
./scripts/clean-build.sh
./scripts/clean-build.sh --all # Also cleans caches and reinstalls dependencies
./scripts/clean-build.sh --clean-gradle-cache # Clean Gradle cache
./scripts/clean-build.sh --clean-derived-data # Clean Xcode DerivedData
./scripts/clean-build.sh --reinstall-node # Reinstall node_modules
# TimeSafari-specific builds # TimeSafari-specific builds
node scripts/build-timesafari.js node scripts/build-timesafari.js
@@ -989,28 +948,6 @@ adb logcat | grep DailyNotification
## Troubleshooting ## Troubleshooting
### Clean Build (First Step for Many Issues)
If you encounter persistent build issues, try a clean build first:
```bash
# Clean all build artifacts (recommended first step)
./scripts/clean-build.sh
# Clean everything including caches (for stubborn issues)
./scripts/clean-build.sh --all
# Then rebuild
./scripts/build-native.sh --platform all
```
**When to use clean-build:**
- Build errors that don't make sense
- Dependency conflicts
- Stale build artifacts
- After switching branches
- After updating dependencies
### Common Issues ### Common Issues
#### Gradle Sync Failures #### Gradle Sync Failures
@@ -1082,39 +1019,6 @@ File → Project Structure → SDK Location
# Solution: Check Kotlin version in build.gradle # Solution: Check Kotlin version in build.gradle
``` ```
#### iOS Build Issues
```bash
# Problem: "Xcode Command Line Tools not configured"
# Error: xcode-select -p fails or xcodebuild not found
# Solution: Install Command Line Tools
xcode-select --install
# Verify installation
xcode-select -p
xcodebuild -version
sqlite3 --version
# Problem: "sqlite3 not found" or linker errors with SQLite
# Solution: Ensure Command Line Tools are properly installed
# The build script checks for this automatically, but if you see linker errors:
xcode-select --install
# Problem: pkgx SQLite conflicts with iOS builds
# Error: Linker errors about libsqlite3.dylib
# Solution: The build script automatically handles this by unsetting problematic
# environment variables. If issues persist:
unset PKGX_DIR DYLD_LIBRARY_PATH LD_LIBRARY_PATH
./scripts/build-native.sh --platform ios
# Problem: "pod install" fails
# Solution: Ensure Command Line Tools are installed
xcode-select --install
# Then reinstall CocoaPods dependencies
cd ios
pod deintegrate
pod install
```
#### Capacitor Integration Issues #### Capacitor Integration Issues
```bash ```bash
# Problem: Plugin not found in Capacitor app # Problem: Plugin not found in Capacitor app

View File

@@ -1,46 +1,47 @@
docs(building): update BUILDING.md with iOS prerequisites and clean-build script fix(build): add SQLite conflict detection and Command Line Tools verification
Updates BUILDING.md to reflect recent changes in build-native.sh, especially Prevents iOS build failures caused by pkgx SQLite linking conflicts and
the Xcode Command Line Tools prerequisite check and the clean-build script. ensures Xcode Command Line Tools are properly installed.
Problem: Problem:
- BUILDING.md didn't mention Xcode Command Line Tools prerequisite - pkgx installs SQLite built for macOS, causing linker errors when building
(recently added to build-native.sh) for iOS simulator: "linking in dylib built for 'macOS'"
- clean-build.sh script exists but wasn't documented - Missing Command Line Tools cause build failures without clear error messages
- iOS build troubleshooting lacked Command Line Tools guidance
Changes: Changes:
- Add Xcode Command Line Tools to Prerequisites section - Add check_sqlite_conflicts() function
- Document installation command (xcode-select --install) - Detects pkgx SQLite installations in ~/.pkgx
- Include verification steps (xcode-select -p, xcodebuild -version) - Warns about macOS dylibs that will cause iOS simulator build failures
- Note that build script automatically checks for these tools - Checks for system SQLite from Command Line Tools
- Explain that sqlite3 is part of Command Line Tools - Validates library paths (DYLD_LIBRARY_PATH, LD_LIBRARY_PATH)
- Document clean-build.sh script in Build Scripts section - Add check_command_line_tools() function
- Basic usage: ./scripts/clean-build.sh - Verifies Xcode Command Line Tools are installed and configured
- All options: --all, --clean-gradle-cache, --clean-derived-data, - Checks for xcodebuild availability
--reinstall-node - Verifies sqlite3 is available (part of Command Line Tools)
- Explain when to use clean builds - Provides clear error messages with installation instructions
- Enhance iOS Native Build Process section - Enhance pkgx detection in iOS build functions
- Add prerequisite note about Command Line Tools - Specifically searches for pkgx SQLite dylibs
- Include troubleshooting commands for pod install issues - Automatically removes pkgx paths from PATH environment variable
- Reference prerequisites section for details - Provides detailed warnings about detected conflicts
- Cleans all problematic environment variables before building
- Add comprehensive troubleshooting sections - Integrate checks into environment validation
- Clean Build section at start of Troubleshooting - Runs automatically when building for iOS
- Recommends clean-build as first step for many issues - Provides early warnings before build starts
- Lists when to use clean builds - Fails fast with clear error messages if tools are missing
- iOS Build Issues section
- Command Line Tools configuration errors
- SQLite/linker issues and pkgx conflicts
- CocoaPods installation problems
- All with clear solutions and commands
The documentation now accurately reflects: This fixes the linker error:
- Xcode Command Line Tools as required iOS prerequisite "ld: building for 'iOS-simulator', but linking in dylib
- clean-build.sh as available build tool (/Users/trent/.pkgx/sqlite.org/v3.44.2/lib/libsqlite3.0.dylib)
- Complete iOS troubleshooting workflow built for 'macOS'"
The build script now:
- Detects pkgx SQLite conflicts before building
- Automatically fixes environment variables
- Verifies Command Line Tools are installed
- Provides clear guidance for manual fixes if needed
Files modified: Files modified:
- BUILDING.md - scripts/build-native.sh

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,28 +239,105 @@ 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"
# Build APK cd "$PROJECT_DIR/android"
if ./gradlew :app:assembleDebug; then
log_info "Android APK built successfully"
APK_PATH="$PROJECT_DIR/android/app/build/outputs/apk/debug/app-debug.apk" # Build APK (Gradle doesn't require adb for building)
if ./gradlew :app:assembleDebug; then
log_info "Android APK built successfully"
if [ -f "$APK_PATH" ]; then APK_PATH="$PROJECT_DIR/android/app/build/outputs/apk/debug/app-debug.apk"
log_info "APK location: $APK_PATH"
# Run on emulator if requested if [ -f "$APK_PATH" ]; then
if [ "$RUN_ALL" = true ] || [ "$RUN_ANDROID" = true ]; then log_info "APK location: $APK_PATH"
# Run on emulator if requested (requires adb)
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,16 +361,16 @@ if [ "$BUILD_ALL" = true ] || [ "$BUILD_ANDROID" = true ]; then
fi fi
fi fi
fi fi
else
log_error "APK not found at expected location: $APK_PATH"
fi fi
else else
log_error "Android build failed" log_error "APK not found at expected location: $APK_PATH"
exit 1
fi fi
else
cd "$PROJECT_DIR" log_error "Android build failed"
exit 1
fi fi
cd "$PROJECT_DIR"
fi fi
# iOS build # iOS build