Merge branch 'master' into ios-implementation

This commit is contained in:
Matthew Raymer
2025-11-11 01:15:55 -08:00
144 changed files with 7764 additions and 4865 deletions

View File

@@ -0,0 +1,286 @@
package com.timesafari.dailynotification;
import static org.junit.Assert.*;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Calendar;
/**
* Instrumentation tests for Daily Notification Plugin
*
* Tests critical notification scheduling and delivery paths
*/
@RunWith(AndroidJUnit4.class)
public class NotificationInstrumentationTest {
private Context appContext;
private AlarmManager alarmManager;
@Rule
public GrantPermissionRule permissionRule = GrantPermissionRule.grant(
android.Manifest.permission.POST_NOTIFICATIONS,
android.Manifest.permission.SCHEDULE_EXACT_ALARM
);
@Before
public void setUp() {
appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
alarmManager = (AlarmManager) appContext.getSystemService(Context.ALARM_SERVICE);
}
@Test
public void testNotifyReceiverRegistration() {
// Verify NotifyReceiver is registered in AndroidManifest
Intent intent = new Intent(appContext, NotifyReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
appContext,
0,
intent,
PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE
);
// If NotifyReceiver is registered, we can create a PendingIntent for it
assertNotNull("NotifyReceiver should be registered in AndroidManifest", pendingIntent);
}
@Test
public void testAlarmScheduling() {
// Test that alarms can be scheduled
long triggerTime = System.currentTimeMillis() + 60000; // 1 minute from now
Intent intent = new Intent(appContext, NotifyReceiver.class);
intent.putExtra("title", "Test Notification");
intent.putExtra("body", "Test body");
int requestCode = NotifyReceiver.Companion.getRequestCode(triggerTime);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
appContext,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
try {
// Use setAlarmClock for Android 5.0+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(
triggerTime,
null
);
alarmManager.setAlarmClock(alarmClockInfo, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
);
} else {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
);
}
// Verify alarm is scheduled
boolean isScheduled = NotifyReceiver.Companion.isAlarmScheduled(appContext, triggerTime);
assertTrue("Alarm should be scheduled", isScheduled);
// Clean up
alarmManager.cancel(pendingIntent);
} catch (SecurityException e) {
fail("Should have permission to schedule exact alarms: " + e.getMessage());
}
}
@Test
public void testUniqueRequestCodes() {
// Test that different trigger times generate different request codes
long triggerTime1 = System.currentTimeMillis() + 60000;
long triggerTime2 = System.currentTimeMillis() + 120000;
int requestCode1 = NotifyReceiver.Companion.getRequestCode(triggerTime1);
int requestCode2 = NotifyReceiver.Companion.getRequestCode(triggerTime2);
// Request codes should be different for different trigger times
assertNotEquals("Different trigger times should generate different request codes",
requestCode1, requestCode2);
}
@Test
public void testAlarmStatusCheck() {
// Test isAlarmScheduled method
long triggerTime = System.currentTimeMillis() + 60000;
// Initially should not be scheduled
boolean initiallyScheduled = NotifyReceiver.Companion.isAlarmScheduled(appContext, triggerTime);
assertFalse("Alarm should not be scheduled initially", initiallyScheduled);
// Schedule alarm
Intent intent = new Intent(appContext, NotifyReceiver.class);
intent.putExtra("title", "Test");
intent.putExtra("body", "Test");
int requestCode = NotifyReceiver.Companion.getRequestCode(triggerTime);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
appContext,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(
triggerTime,
null
);
alarmManager.setAlarmClock(alarmClockInfo, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
);
} else {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
);
}
// Now should be scheduled
boolean afterScheduling = NotifyReceiver.Companion.isAlarmScheduled(appContext, triggerTime);
assertTrue("Alarm should be scheduled after calling setAlarmClock", afterScheduling);
// Clean up
alarmManager.cancel(pendingIntent);
} catch (SecurityException e) {
fail("Should have permission to schedule exact alarms: " + e.getMessage());
}
}
@Test
public void testNextAlarmTime() {
// Test getNextAlarmTime method (requires Android 5.0+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Long nextAlarmTime = NotifyReceiver.Companion.getNextAlarmTime(appContext);
// May be null if no alarm scheduled, which is valid
if (nextAlarmTime != null) {
assertTrue("Next alarm time should be in the future",
nextAlarmTime > System.currentTimeMillis());
}
}
}
@Test
public void testAlarmCancellation() {
// Test that alarms can be cancelled
long triggerTime = System.currentTimeMillis() + 60000;
Intent intent = new Intent(appContext, NotifyReceiver.class);
intent.putExtra("title", "Test");
intent.putExtra("body", "Test");
int requestCode = NotifyReceiver.Companion.getRequestCode(triggerTime);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
appContext,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
try {
// Schedule alarm
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(
triggerTime,
null
);
alarmManager.setAlarmClock(alarmClockInfo, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
);
} else {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
);
}
// Verify scheduled
assertTrue("Alarm should be scheduled",
NotifyReceiver.Companion.isAlarmScheduled(appContext, triggerTime));
// Cancel alarm
NotifyReceiver.Companion.cancelNotification(appContext, triggerTime);
// Verify cancelled
assertFalse("Alarm should be cancelled",
NotifyReceiver.Companion.isAlarmScheduled(appContext, triggerTime));
} catch (SecurityException e) {
fail("Should have permission to schedule exact alarms: " + e.getMessage());
}
}
@Test
public void testPendingIntentUniqueness() {
// Test that PendingIntents with different request codes don't conflict
long triggerTime1 = System.currentTimeMillis() + 60000;
long triggerTime2 = System.currentTimeMillis() + 120000;
Intent intent1 = new Intent(appContext, NotifyReceiver.class);
intent1.putExtra("title", "Test 1");
intent1.putExtra("body", "Test 1");
Intent intent2 = new Intent(appContext, NotifyReceiver.class);
intent2.putExtra("title", "Test 2");
intent2.putExtra("body", "Test 2");
int requestCode1 = NotifyReceiver.Companion.getRequestCode(triggerTime1);
int requestCode2 = NotifyReceiver.Companion.getRequestCode(triggerTime2);
PendingIntent pendingIntent1 = PendingIntent.getBroadcast(
appContext,
requestCode1,
intent1,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
PendingIntent pendingIntent2 = PendingIntent.getBroadcast(
appContext,
requestCode2,
intent2,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// Both PendingIntents should be created successfully
assertNotNull("First PendingIntent should be created", pendingIntent1);
assertNotNull("Second PendingIntent should be created", pendingIntent2);
// They should be different objects
assertNotSame("PendingIntents should be different objects",
pendingIntent1, pendingIntent2);
}
}

View File

@@ -36,6 +36,13 @@
</intent-filter>
</receiver>
<!-- NotifyReceiver for AlarmManager-based notifications -->
<receiver
android:name="com.timesafari.dailynotification.NotifyReceiver"
android:enabled="true"
android:exported="false">
</receiver>
<receiver
android:name="com.timesafari.dailynotification.BootReceiver"
android:enabled="true"

View File

@@ -3,6 +3,4 @@ include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':timesafari-daily-notification-plugin'
// NOTE: Plugin module is in android/plugin/ subdirectory, not android root
// This file is auto-generated by Capacitor, but must be manually corrected for this plugin structure
project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android/plugin')
project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')

View File

@@ -5,10 +5,10 @@
*
* Fixes:
* 1. capacitor.plugins.json - Ensures DailyNotification plugin is registered
* 2. capacitor.settings.gradle - Corrects plugin path from android/ to android/plugin/
* 2. capacitor.settings.gradle - Verifies plugin path points to android/ (standard structure)
*
* This script should run automatically after 'npx cap sync android'
* to fix issues with Capacitor's auto-generated files.
* to verify Capacitor's auto-generated files are correct.
*
* @author Matthew Raymer
*/
@@ -60,10 +60,10 @@ function fixCapacitorPlugins() {
}
/**
* Fix capacitor.settings.gradle to point to android/plugin/ instead of android/
* Fix capacitor.settings.gradle to verify plugin path points to android/ (standard structure)
*/
function fixCapacitorSettingsGradle() {
console.log('🔧 Fixing capacitor.settings.gradle...');
console.log('🔧 Verifying capacitor.settings.gradle...');
if (!fs.existsSync(SETTINGS_GRADLE_PATH)) {
console.log(' capacitor.settings.gradle not found (may not be a test-app)');
@@ -74,30 +74,31 @@ function fixCapacitorSettingsGradle() {
let content = fs.readFileSync(SETTINGS_GRADLE_PATH, 'utf8');
const originalContent = content;
// Check if the path already points to android/plugin
if (content.includes('android/plugin')) {
console.log('✅ capacitor.settings.gradle already has correct path (android/plugin)');
return;
}
// Check if the path correctly points to android/ (standard structure)
const correctPath = "project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')";
const oldPluginPath = "project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android/plugin')";
// Check if we need to fix the path (points to android but should be android/plugin)
if (content.includes("project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')")) {
// Replace the path
// Check if it's using the old android/plugin/ path (needs fixing)
if (content.includes('android/plugin')) {
console.log('⚠️ capacitor.settings.gradle uses old path (android/plugin/) - fixing to standard structure');
content = content.replace(
"project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android')",
`// NOTE: Plugin module is in android/plugin/ subdirectory, not android root
// This file is auto-generated by Capacitor, but must be manually corrected for this plugin structure
project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android/plugin')`
oldPluginPath,
`// Plugin uses standard Capacitor structure: android/ (not android/plugin/)
${correctPath}`
);
fs.writeFileSync(SETTINGS_GRADLE_PATH, content);
console.log('✅ Fixed plugin path in capacitor.settings.gradle (android -> android/plugin)');
if (content !== originalContent) {
fs.writeFileSync(SETTINGS_GRADLE_PATH, content);
console.log('✅ Fixed plugin path in capacitor.settings.gradle (android/plugin -> android)');
}
} else if (content.includes(correctPath) || content.includes("android')")) {
console.log('✅ capacitor.settings.gradle has correct path (android/)');
} else {
console.log(' capacitor.settings.gradle doesn\'t reference the plugin or uses a different structure');
}
} catch (error) {
console.error('❌ Error fixing capacitor.settings.gradle:', error.message);
console.error('❌ Error verifying capacitor.settings.gradle:', error.message);
process.exit(1);
}
}