refactor(android,ios): rename package com.timesafari to org.timesafari.dailynotification

- Android: move plugin source to org/timesafari/dailynotification, update
  namespace, manifest package, and all package/imports; change intent actions
  to org.timesafari.daily.NOTIFICATION and DISMISS
- iOS: update bundle IDs, BGTask identifiers, subsystem labels, and queue
  names in Plugin and Xcode projects
- Capacitor: update plugin class registration and appIds in configs
- Test apps (android-test-app, daily-notification-test, ios-test-app):
  applicationId/bundleId, manifests, ProGuard, scripts, and docs
- Docs: bulk update references; add CONSUMING_APP_MIGRATION_COM_TO_ORG.md
  for consuming app migration

BREAKING CHANGE: Consuming apps must update plugin class to
org.timesafari.dailynotification.DailyNotificationPlugin, manifest
receivers/actions, and iOS BGTask identifiers per migration doc.
This commit is contained in:
Jose Olarte III
2026-03-12 14:26:07 +08:00
parent b8d9b6247d
commit d8a0eaf413
171 changed files with 749 additions and 646 deletions

View File

@@ -1239,10 +1239,10 @@ dependencies {
-keep @androidx.room.Dao class * -keep @androidx.room.Dao class *
# Plugin classes # Plugin classes
-keep class com.timesafari.dailynotification.** { *; } -keep class org.timesafari.dailynotification.** { *; }
# Capacitor plugin # Capacitor plugin
-keep class com.timesafari.dailynotification.DailyNotificationPlugin { *; } -keep class org.timesafari.dailynotification.DailyNotificationPlugin { *; }
# Encryption # Encryption
-keep class javax.crypto.** { *; } -keep class javax.crypto.** { *; }

View File

@@ -653,7 +653,7 @@ public class MainActivity extends BridgeActivity {
{ {
"plugins": { "plugins": {
"DailyNotification": { "DailyNotification": {
"class": "com.timesafari.dailynotification.DailyNotificationPlugin" "class": "org.timesafari.dailynotification.DailyNotificationPlugin"
} }
} }
} }
@@ -728,7 +728,7 @@ The Vue 3 test app uses a **project reference approach** for plugin integration:
{ {
"id": "DailyNotification", "id": "DailyNotification",
"name": "DailyNotification", "name": "DailyNotification",
"class": "com.timesafari.dailynotification.DailyNotificationPlugin" "class": "org.timesafari.dailynotification.DailyNotificationPlugin"
} }
] ]
``` ```
@@ -1128,7 +1128,7 @@ npx cap sync android
#### AAR Duplicate Class Issues #### AAR Duplicate Class Issues
```bash ```bash
# Problem: Duplicate class errors when integrating plugin AAR # Problem: Duplicate class errors when integrating plugin AAR
# Error: "Duplicate class com.timesafari.dailynotification.BootReceiver found in modules" # Error: "Duplicate class org.timesafari.dailynotification.BootReceiver found in modules"
# Root Cause: Plugin being included both as project reference and as AAR file # Root Cause: Plugin being included both as project reference and as AAR file
# Solution 1: Use Project Reference Approach (Recommended) # Solution 1: Use Project Reference Approach (Recommended)

View File

@@ -564,12 +564,12 @@ await DailyNotification.updateDailyReminder('morning_checkin', {
<!-- NotifyReceiver for AlarmManager-based notifications --> <!-- NotifyReceiver for AlarmManager-based notifications -->
<!-- REQUIRED: Without this, alarms fire but notifications won't display --> <!-- REQUIRED: Without this, alarms fire but notifications won't display -->
<receiver android:name="com.timesafari.dailynotification.NotifyReceiver" <receiver android:name="org.timesafari.dailynotification.NotifyReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
</receiver> </receiver>
<receiver android:name="com.timesafari.dailynotification.BootReceiver" <receiver android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
@@ -604,8 +604,8 @@ dependencies {
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.content-fetch</string> <string>org.timesafari.dailynotification.content-fetch</string>
<string>com.timesafari.dailynotification.notification-delivery</string> <string>org.timesafari.dailynotification.notification-delivery</string>
</array> </array>
``` ```

View File

@@ -14,7 +14,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
namespace "com.timesafari.dailynotification.plugin" namespace "org.timesafari.dailynotification.plugin"
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35 compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
defaultConfig { defaultConfig {

View File

@@ -2,7 +2,7 @@
# These rules are applied to consuming apps when they use this plugin # These rules are applied to consuming apps when they use this plugin
# Keep plugin classes # Keep plugin classes
-keep class com.timesafari.dailynotification.** { *; } -keep class org.timesafari.dailynotification.** { *; }
# Keep Capacitor plugin interface # Keep Capacitor plugin interface
-keep class com.getcapacitor.Plugin { *; } -keep class com.getcapacitor.Plugin { *; }

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.timesafari.dailynotification.plugin"> package="org.timesafari.dailynotification.plugin">
<!-- Plugin receivers are declared in consuming app's manifest --> <!-- Plugin receivers are declared in consuming app's manifest -->
<!-- This manifest is optional and mainly for library metadata --> <!-- This manifest is optional and mainly for library metadata -->

View File

@@ -2,7 +2,7 @@
{ {
"pkg": "@timesafari/daily-notification-plugin", "pkg": "@timesafari/daily-notification-plugin",
"name": "DailyNotification", "name": "DailyNotification",
"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin" "classpath": "org.timesafari.dailynotification.DailyNotificationPlugin"
} }
] ]

View File

@@ -1,4 +1,4 @@
package com.timesafari.dailynotification package org.timesafari.dailynotification
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context

View File

@@ -1,4 +1,4 @@
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
@@ -43,7 +43,7 @@ public class ChannelManager {
Log.d(TAG, "Ensuring notification channel exists"); Log.d(TAG, "Ensuring notification channel exists");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = notificationManager.getNotificationChannel(com.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID); NotificationChannel channel = notificationManager.getNotificationChannel(org.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID);
if (channel == null) { if (channel == null) {
Log.d(TAG, "Creating notification channel"); Log.d(TAG, "Creating notification channel");
@@ -72,7 +72,7 @@ public class ChannelManager {
public boolean isChannelEnabled() { public boolean isChannelEnabled() {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = notificationManager.getNotificationChannel(com.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID); NotificationChannel channel = notificationManager.getNotificationChannel(org.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID);
if (channel == null) { if (channel == null) {
Log.w(TAG, "Channel does not exist"); Log.w(TAG, "Channel does not exist");
return false; return false;
@@ -99,7 +99,7 @@ public class ChannelManager {
public int getChannelImportance() { public int getChannelImportance() {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = notificationManager.getNotificationChannel(com.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID); NotificationChannel channel = notificationManager.getNotificationChannel(org.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID);
if (channel != null) { if (channel != null) {
return channel.getImportance(); return channel.getImportance();
} }
@@ -117,7 +117,7 @@ public class ChannelManager {
* @return true if settings intent was launched, false otherwise * @return true if settings intent was launched, false otherwise
*/ */
public boolean openChannelSettings() { public boolean openChannelSettings() {
return openChannelSettings(com.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID); return openChannelSettings(org.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID);
} }
/** /**
@@ -142,7 +142,7 @@ public class ChannelManager {
try { try {
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()) .putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName())
.putExtra(Settings.EXTRA_CHANNEL_ID, channelId != null ? channelId : com.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID) .putExtra(Settings.EXTRA_CHANNEL_ID, channelId != null ? channelId : org.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent); context.startActivity(intent);
@@ -180,11 +180,11 @@ public class ChannelManager {
private void createDefaultChannel() { private void createDefaultChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel( NotificationChannel channel = new NotificationChannel(
com.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID, org.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID,
com.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_NAME, org.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH NotificationManager.IMPORTANCE_HIGH
); );
channel.setDescription(com.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_DESCRIPTION); channel.setDescription(org.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_DESCRIPTION);
channel.enableLights(true); channel.enableLights(true);
channel.enableVibration(true); channel.enableVibration(true);
channel.setShowBadge(true); channel.setShowBadge(true);
@@ -200,7 +200,7 @@ public class ChannelManager {
* @return the default channel ID * @return the default channel ID
*/ */
public String getDefaultChannelId() { public String getDefaultChannelId() {
return com.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID; return org.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID;
} }
/** /**
@@ -209,7 +209,7 @@ public class ChannelManager {
public void logChannelStatus() { public void logChannelStatus() {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = notificationManager.getNotificationChannel(com.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID); NotificationChannel channel = notificationManager.getNotificationChannel(org.timesafari.dailynotification.DailyNotificationConstants.DEFAULT_CHANNEL_ID);
if (channel != null) { if (channel != null) {
Log.i(TAG, "Channel Status - ID: " + channel.getId() + Log.i(TAG, "Channel Status - ID: " + channel.getId() +
", Importance: " + channel.getImportance() + ", Importance: " + channel.getImportance() +

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification package org.timesafari.dailynotification
/** /**
* Centralized constants for Daily Notification Plugin * Centralized constants for Daily Notification Plugin
@@ -56,7 +56,7 @@ object DailyNotificationConstants {
* Action string for notification broadcast intents * Action string for notification broadcast intents
* Used by AlarmManager PendingIntents * Used by AlarmManager PendingIntents
*/ */
const val ACTION_NOTIFICATION = "com.timesafari.daily.NOTIFICATION" const val ACTION_NOTIFICATION = "org.timesafari.daily.NOTIFICATION"
// ============================================================ // ============================================================
// Intent Extras Keys // Intent Extras Keys

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.util.Log; import android.util.Log;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.util.Log; import android.util.Log;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
@@ -42,7 +42,7 @@ public class DailyNotificationFetcher {
private final Context context; private final Context context;
private final DailyNotificationStorage storage; // Deprecated path (kept for transitional read paths) private final DailyNotificationStorage storage; // Deprecated path (kept for transitional read paths)
private final com.timesafari.dailynotification.storage.DailyNotificationStorageRoom roomStorage; // Preferred path private final org.timesafari.dailynotification.storage.DailyNotificationStorageRoom roomStorage; // Preferred path
private final WorkManager workManager; private final WorkManager workManager;
// ETag manager for efficient fetching // ETag manager for efficient fetching
@@ -60,7 +60,7 @@ public class DailyNotificationFetcher {
public DailyNotificationFetcher(Context context, public DailyNotificationFetcher(Context context,
DailyNotificationStorage storage, DailyNotificationStorage storage,
com.timesafari.dailynotification.storage.DailyNotificationStorageRoom roomStorage) { org.timesafari.dailynotification.storage.DailyNotificationStorageRoom roomStorage) {
this.context = context; this.context = context;
this.storage = storage; this.storage = storage;
this.roomStorage = roomStorage; this.roomStorage = roomStorage;
@@ -220,8 +220,8 @@ public class DailyNotificationFetcher {
return; return;
} }
try { try {
com.timesafari.dailynotification.entities.NotificationContentEntity entity = org.timesafari.dailynotification.entities.NotificationContentEntity entity =
new com.timesafari.dailynotification.entities.NotificationContentEntity( new org.timesafari.dailynotification.entities.NotificationContentEntity(
content.getId() != null ? content.getId() : java.util.UUID.randomUUID().toString(), content.getId() != null ? content.getId() : java.util.UUID.randomUUID().toString(),
"1.0.0", "1.0.0",
null, null,

View File

@@ -9,7 +9,7 @@
* @created 2025-10-03 06:53:30 UTC * @created 2025-10-03 06:53:30 UTC
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.util.Log; import android.util.Log;
import android.content.Context; import android.content.Context;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.os.Debug; import android.os.Debug;

View File

@@ -1,4 +1,4 @@
package com.timesafari.dailynotification package org.timesafari.dailynotification
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
@@ -18,7 +18,7 @@ import androidx.work.WorkManager
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.Data import androidx.work.Data
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import com.timesafari.dailynotification.DailyNotificationFetchWorker import org.timesafari.dailynotification.DailyNotificationFetchWorker
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
import com.getcapacitor.Plugin import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall import com.getcapacitor.PluginCall
@@ -585,7 +585,7 @@ open class DailyNotificationPlugin : Plugin() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val config = com.timesafari.dailynotification.entities.NotificationConfigEntity( val config = org.timesafari.dailynotification.entities.NotificationConfigEntity(
configId, null, "native_fetcher", "config", configValue, "json" configId, null, "native_fetcher", "config", configValue, "json"
) )
getDatabase().notificationConfigDao().insertConfig(config) getDatabase().notificationConfigDao().insertConfig(config)
@@ -2153,7 +2153,7 @@ open class DailyNotificationPlugin : Plugin() {
?: return@launch call.reject("Config value is required") ?: return@launch call.reject("Config value is required")
val configDataType = configJson.getString("configDataType", "string") val configDataType = configJson.getString("configDataType", "string")
val entity = com.timesafari.dailynotification.entities.NotificationConfigEntity( val entity = org.timesafari.dailynotification.entities.NotificationConfigEntity(
id, timesafariDid, configType, configKey, configValue, configDataType id, timesafariDid, configType, configKey, configValue, configDataType
) )
@@ -2285,7 +2285,7 @@ open class DailyNotificationPlugin : Plugin() {
} }
} }
private fun configToJson(config: com.timesafari.dailynotification.entities.NotificationConfigEntity): JSObject { private fun configToJson(config: org.timesafari.dailynotification.entities.NotificationConfigEntity): JSObject {
return JSObject().apply { return JSObject().apply {
put("id", config.id) put("id", config.id)
put("timesafariDid", config.timesafariDid) put("timesafariDid", config.timesafariDid)
@@ -2474,7 +2474,7 @@ object TestDataHelper {
suspend fun injectInvalidNotificationData(database: DailyNotificationDatabase): Boolean { suspend fun injectInvalidNotificationData(database: DailyNotificationDatabase): Boolean {
return try { return try {
val invalidNotification = val invalidNotification =
com.timesafari.dailynotification.entities.NotificationContentEntity() org.timesafari.dailynotification.entities.NotificationContentEntity()
invalidNotification.id = "" // Empty ID - should be skipped by recovery invalidNotification.id = "" // Empty ID - should be skipped by recovery
invalidNotification.title = "Test Invalid Notification" invalidNotification.title = "Test Invalid Notification"
invalidNotification.body = "This has an empty ID" invalidNotification.body = "This has an empty ID"
@@ -2683,7 +2683,7 @@ object ScheduleHelper {
// Persist title/body for this scheduleId so rollover and post-reboot resolve user content // Persist title/body for this scheduleId so rollover and post-reboot resolve user content
// (see plugin-feedback-android-rollover-double-fire-and-user-content) // (see plugin-feedback-android-rollover-double-fire-and-user-content)
try { try {
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity( val entity = org.timesafari.dailynotification.entities.NotificationContentEntity(
scheduleId, scheduleId,
"1.3.1", "1.3.1",
null, null,

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
@@ -59,7 +59,7 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
return; return;
} }
if ("com.timesafari.daily.NOTIFICATION".equals(action)) { if ("org.timesafari.daily.NOTIFICATION".equals(action)) {
// Parse intent and enqueue work - keep receiver ultra-light // Parse intent and enqueue work - keep receiver ultra-light
String notificationId = intent.getStringExtra(EXTRA_NOTIFICATION_ID); String notificationId = intent.getStringExtra(EXTRA_NOTIFICATION_ID);
if (notificationId == null) { if (notificationId == null) {
@@ -72,7 +72,7 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
enqueueNotificationWork(context, notificationId, intent); enqueueNotificationWork(context, notificationId, intent);
Log.d(TAG, "DN|RECEIVE_OK enqueued=" + notificationId); Log.d(TAG, "DN|RECEIVE_OK enqueued=" + notificationId);
} else if ("com.timesafari.daily.DISMISS".equals(action)) { } else if ("org.timesafari.daily.DISMISS".equals(action)) {
// Handle dismissal - also lightweight // Handle dismissal - also lightweight
String notificationId = intent.getStringExtra(EXTRA_NOTIFICATION_ID); String notificationId = intent.getStringExtra(EXTRA_NOTIFICATION_ID);
if (notificationId != null) { if (notificationId != null) {
@@ -362,7 +362,7 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
// Add dismiss action // Add dismiss action
Intent dismissIntent = new Intent(context, DailyNotificationReceiver.class); Intent dismissIntent = new Intent(context, DailyNotificationReceiver.class);
dismissIntent.setAction("com.timesafari.daily.DISMISS"); dismissIntent.setAction("org.timesafari.daily.DISMISS");
dismissIntent.putExtra(EXTRA_NOTIFICATION_ID, content.getId()); dismissIntent.putExtra(EXTRA_NOTIFICATION_ID, content.getId());
PendingIntent dismissPendingIntent = PendingIntent.getBroadcast( PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(
@@ -432,8 +432,8 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
} }
// Create config for next notification // Create config for next notification
com.timesafari.dailynotification.UserNotificationConfig config = org.timesafari.dailynotification.UserNotificationConfig config =
new com.timesafari.dailynotification.UserNotificationConfig( new org.timesafari.dailynotification.UserNotificationConfig(
true, // enabled true, // enabled
cronExpression, cronExpression,
content.getTitle() != null ? content.getTitle() : "Daily Notification", content.getTitle() != null ? content.getTitle() : "Daily Notification",
@@ -444,14 +444,14 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
); );
// Use centralized scheduling function with ROLLOVER_ON_FIRE source // Use centralized scheduling function with ROLLOVER_ON_FIRE source
com.timesafari.dailynotification.NotifyReceiver.scheduleExactNotification( org.timesafari.dailynotification.NotifyReceiver.scheduleExactNotification(
context, context,
nextScheduledTime, nextScheduledTime,
config, config,
false, // isStaticReminder false, // isStaticReminder
null, // reminderId null, // reminderId
scheduleId, scheduleId,
com.timesafari.dailynotification.ScheduleSource.ROLLOVER_ON_FIRE, org.timesafari.dailynotification.ScheduleSource.ROLLOVER_ON_FIRE,
false // skipPendingIntentIdempotence rollover path does not skip false // skipPendingIntentIdempotence rollover path does not skip
); );

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
@@ -157,8 +157,8 @@ public class DailyNotificationScheduler {
// Create intent for the notification; setPackage ensures AlarmManager delivery on all OEMs // Create intent for the notification; setPackage ensures AlarmManager delivery on all OEMs
Intent intent = new Intent(context, DailyNotificationReceiver.class); Intent intent = new Intent(context, DailyNotificationReceiver.class);
intent.setPackage(context.getPackageName()); intent.setPackage(context.getPackageName());
intent.setAction(com.timesafari.dailynotification.DailyNotificationConstants.ACTION_NOTIFICATION); intent.setAction(org.timesafari.dailynotification.DailyNotificationConstants.ACTION_NOTIFICATION);
intent.putExtra(com.timesafari.dailynotification.DailyNotificationConstants.EXTRA_NOTIFICATION_ID, content.getId()); intent.putExtra(org.timesafari.dailynotification.DailyNotificationConstants.EXTRA_NOTIFICATION_ID, content.getId());
// Check if this is a static reminder // Check if this is a static reminder
if (content.getId().startsWith("reminder_") || content.getId().contains("_reminder")) { if (content.getId().startsWith("reminder_") || content.getId().contains("_reminder")) {
@@ -481,7 +481,7 @@ public class DailyNotificationScheduler {
try { try {
Log.d(TAG, "Scheduling test alarm in " + secondsFromNow + " seconds"); Log.d(TAG, "Scheduling test alarm in " + secondsFromNow + " seconds");
// Delegate to NotifyReceiver.testAlarm() // Delegate to NotifyReceiver.testAlarm()
com.timesafari.dailynotification.NotifyReceiver.Companion.testAlarm(context, secondsFromNow); org.timesafari.dailynotification.NotifyReceiver.Companion.testAlarm(context, secondsFromNow);
Log.i(TAG, "Test alarm scheduled successfully"); Log.i(TAG, "Test alarm scheduled successfully");
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Error scheduling test alarm", e); Log.e(TAG, "Error scheduling test alarm", e);
@@ -591,7 +591,7 @@ public class DailyNotificationScheduler {
// Note: NotifyReceiver.isAlarmScheduled is a Kotlin companion object function with default parameters // Note: NotifyReceiver.isAlarmScheduled is a Kotlin companion object function with default parameters
// From Java, we need to use Companion and provide explicit values (null is acceptable for optional params) // From Java, we need to use Companion and provide explicit values (null is acceptable for optional params)
// Kotlin Long? maps to java.lang.Long in Java // Kotlin Long? maps to java.lang.Long in Java
return com.timesafari.dailynotification.NotifyReceiver.Companion.isAlarmScheduled( return org.timesafari.dailynotification.NotifyReceiver.Companion.isAlarmScheduled(
context, context,
scheduleId, scheduleId,
triggerAtMillis triggerAtMillis
@@ -624,7 +624,7 @@ public class DailyNotificationScheduler {
// Delegate to NotifyReceiver which checks actual AlarmManager state // Delegate to NotifyReceiver which checks actual AlarmManager state
// Note: NotifyReceiver.getNextAlarmTime is a Kotlin companion object function // Note: NotifyReceiver.getNextAlarmTime is a Kotlin companion object function
// Kotlin Long? maps to java.lang.Long in Java // Kotlin Long? maps to java.lang.Long in Java
return com.timesafari.dailynotification.NotifyReceiver.Companion.getNextAlarmTime(context); return org.timesafari.dailynotification.NotifyReceiver.Companion.getNextAlarmTime(context);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Error getting next alarm time", e); Log.e(TAG, "Error getting next alarm time", e);
return null; return null;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
@@ -30,9 +30,9 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import com.timesafari.dailynotification.storage.DailyNotificationStorageRoom; import org.timesafari.dailynotification.storage.DailyNotificationStorageRoom;
import com.timesafari.dailynotification.entities.NotificationContentEntity; import org.timesafari.dailynotification.entities.NotificationContentEntity;
import com.timesafari.dailynotification.DailyNotificationFetcher; import org.timesafari.dailynotification.DailyNotificationFetcher;
/** /**
* WorkManager worker for processing daily notifications * WorkManager worker for processing daily notifications
@@ -382,7 +382,7 @@ public class DailyNotificationWorker extends Worker {
// Create one-time work request // Create one-time work request
androidx.work.OneTimeWorkRequest softRefetchWork = new androidx.work.OneTimeWorkRequest.Builder( androidx.work.OneTimeWorkRequest softRefetchWork = new androidx.work.OneTimeWorkRequest.Builder(
com.timesafari.dailynotification.SoftRefetchWorker.class) org.timesafari.dailynotification.SoftRefetchWorker.class)
.setConstraints(constraints) .setConstraints(constraints)
.setInputData(inputData) .setInputData(inputData)
.setInitialDelay(softRefetchTime - System.currentTimeMillis(), java.util.concurrent.TimeUnit.MILLISECONDS) .setInitialDelay(softRefetchTime - System.currentTimeMillis(), java.util.concurrent.TimeUnit.MILLISECONDS)
@@ -469,7 +469,7 @@ public class DailyNotificationWorker extends Worker {
// Add action buttons // Add action buttons
// 1. Dismiss action // 1. Dismiss action
Intent dismissIntent = new Intent(getApplicationContext(), DailyNotificationReceiver.class); Intent dismissIntent = new Intent(getApplicationContext(), DailyNotificationReceiver.class);
dismissIntent.setAction("com.timesafari.daily.DISMISS"); dismissIntent.setAction("org.timesafari.daily.DISMISS");
dismissIntent.putExtra("notification_id", content.getId()); dismissIntent.putExtra("notification_id", content.getId());
PendingIntent dismissPendingIntent = PendingIntent.getBroadcast( PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(
@@ -550,14 +550,14 @@ public class DailyNotificationWorker extends Worker {
// When firing run used daily_rollover_* id, resolve canonical schedule so we still apply rolloverIntervalMinutes // When firing run used daily_rollover_* id, resolve canonical schedule so we still apply rolloverIntervalMinutes
String logicalScheduleIdForRollover = scheduleIdForRollover; String logicalScheduleIdForRollover = scheduleIdForRollover;
if (scheduleIdForRollover != null && scheduleIdForRollover.startsWith("daily_rollover_")) { if (scheduleIdForRollover != null && scheduleIdForRollover.startsWith("daily_rollover_")) {
com.timesafari.dailynotification.Schedule canonical = com.timesafari.dailynotification.ScheduleHelper.getCanonicalRolloverScheduleBlocking(getApplicationContext()); org.timesafari.dailynotification.Schedule canonical = org.timesafari.dailynotification.ScheduleHelper.getCanonicalRolloverScheduleBlocking(getApplicationContext());
if (canonical != null) { if (canonical != null) {
logicalScheduleIdForRollover = canonical.getId(); logicalScheduleIdForRollover = canonical.getId();
} }
} }
Integer rolloverMinutes = null; Integer rolloverMinutes = null;
if (logicalScheduleIdForRollover != null && !logicalScheduleIdForRollover.isEmpty()) { if (logicalScheduleIdForRollover != null && !logicalScheduleIdForRollover.isEmpty()) {
com.timesafari.dailynotification.Schedule s = com.timesafari.dailynotification.ScheduleHelper.getScheduleBlocking(getApplicationContext(), logicalScheduleIdForRollover); org.timesafari.dailynotification.Schedule s = org.timesafari.dailynotification.ScheduleHelper.getScheduleBlocking(getApplicationContext(), logicalScheduleIdForRollover);
if (s != null && s.getRolloverIntervalMinutes() != null && s.getRolloverIntervalMinutes() > 0) { if (s != null && s.getRolloverIntervalMinutes() != null && s.getRolloverIntervalMinutes() > 0) {
rolloverMinutes = s.getRolloverIntervalMinutes(); rolloverMinutes = s.getRolloverIntervalMinutes();
Log.d(TAG, "DN|ROLLOVER_INTERVAL scheduleId=" + logicalScheduleIdForRollover + " minutes=" + rolloverMinutes); Log.d(TAG, "DN|ROLLOVER_INTERVAL scheduleId=" + logicalScheduleIdForRollover + " minutes=" + rolloverMinutes);
@@ -621,8 +621,8 @@ public class DailyNotificationWorker extends Worker {
} }
// Create config for next notification // Create config for next notification
com.timesafari.dailynotification.UserNotificationConfig config = org.timesafari.dailynotification.UserNotificationConfig config =
new com.timesafari.dailynotification.UserNotificationConfig( new org.timesafari.dailynotification.UserNotificationConfig(
true, // enabled true, // enabled
cronExpression, cronExpression,
content.getTitle() != null ? content.getTitle() : "Daily Notification", content.getTitle() != null ? content.getTitle() : "Daily Notification",
@@ -634,18 +634,18 @@ public class DailyNotificationWorker extends Worker {
// Use centralized scheduling function with ROLLOVER_ON_FIRE source // Use centralized scheduling function with ROLLOVER_ON_FIRE source
Log.d(TAG, "DN|ROLLOVER next=" + nextScheduledTime + " scheduleId=" + scheduleId + " static=" + preserveStaticReminder); Log.d(TAG, "DN|ROLLOVER next=" + nextScheduledTime + " scheduleId=" + scheduleId + " static=" + preserveStaticReminder);
com.timesafari.dailynotification.NotifyReceiver.scheduleExactNotification( org.timesafari.dailynotification.NotifyReceiver.scheduleExactNotification(
getApplicationContext(), getApplicationContext(),
nextScheduledTime, nextScheduledTime,
config, config,
preserveStaticReminder, // isStaticReminder preserve so next run keeps title/body preserveStaticReminder, // isStaticReminder preserve so next run keeps title/body
preserveStaticReminder ? scheduleId : null, // reminderId preserveStaticReminder ? scheduleId : null, // reminderId
scheduleId, scheduleId,
com.timesafari.dailynotification.ScheduleSource.ROLLOVER_ON_FIRE, org.timesafari.dailynotification.ScheduleSource.ROLLOVER_ON_FIRE,
false // skipPendingIntentIdempotence rollover path does not skip false // skipPendingIntentIdempotence rollover path does not skip
); );
if (scheduleId != null && !scheduleId.startsWith("daily_rollover_")) { if (scheduleId != null && !scheduleId.startsWith("daily_rollover_")) {
com.timesafari.dailynotification.ScheduleHelper.updateScheduleNextRunTimeBlocking( org.timesafari.dailynotification.ScheduleHelper.updateScheduleNextRunTimeBlocking(
getApplicationContext(), scheduleId, content.getScheduledTime(), nextScheduledTime); getApplicationContext(), scheduleId, content.getScheduledTime(), nextScheduledTime);
} }
// Log next scheduled time in readable format // Log next scheduled time in readable format
@@ -693,8 +693,8 @@ public class DailyNotificationWorker extends Worker {
private NotificationContent getContentByScheduleId(String scheduleId) { private NotificationContent getContentByScheduleId(String scheduleId) {
if (scheduleId == null || scheduleId.isEmpty()) return null; if (scheduleId == null || scheduleId.isEmpty()) return null;
try { try {
com.timesafari.dailynotification.DailyNotificationDatabase db = org.timesafari.dailynotification.DailyNotificationDatabase db =
com.timesafari.dailynotification.DailyNotificationDatabase.getInstance(getApplicationContext()); org.timesafari.dailynotification.DailyNotificationDatabase.getInstance(getApplicationContext());
NotificationContentEntity entity = db.notificationContentDao().getNotificationById(scheduleId); NotificationContentEntity entity = db.notificationContentDao().getNotificationById(scheduleId);
if (entity == null) { if (entity == null) {
entity = db.notificationContentDao().getNotificationById("daily_" + scheduleId); entity = db.notificationContentDao().getNotificationById("daily_" + scheduleId);
@@ -716,8 +716,8 @@ public class DailyNotificationWorker extends Worker {
try { try {
DailyNotificationStorageRoom room = new DailyNotificationStorageRoom(getApplicationContext()); DailyNotificationStorageRoom room = new DailyNotificationStorageRoom(getApplicationContext());
// Use unified database (Kotlin schema with Java entities) // Use unified database (Kotlin schema with Java entities)
com.timesafari.dailynotification.DailyNotificationDatabase db = org.timesafari.dailynotification.DailyNotificationDatabase db =
com.timesafari.dailynotification.DailyNotificationDatabase.getInstance(getApplicationContext()); org.timesafari.dailynotification.DailyNotificationDatabase.getInstance(getApplicationContext());
NotificationContentEntity entity = db.notificationContentDao().getNotificationById(notificationId); NotificationContentEntity entity = db.notificationContentDao().getNotificationById(notificationId);
if (entity != null) { if (entity != null) {
return mapEntityToContent(entity); return mapEntityToContent(entity);

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
/** /**
* Information about a scheduled daily reminder * Information about a scheduled daily reminder

View File

@@ -9,7 +9,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;

View File

@@ -1,15 +1,15 @@
package com.timesafari.dailynotification package org.timesafari.dailynotification
import android.content.Context import android.content.Context
import androidx.room.* import androidx.room.*
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.timesafari.dailynotification.entities.NotificationContentEntity import org.timesafari.dailynotification.entities.NotificationContentEntity
import com.timesafari.dailynotification.entities.NotificationDeliveryEntity import org.timesafari.dailynotification.entities.NotificationDeliveryEntity
import com.timesafari.dailynotification.entities.NotificationConfigEntity import org.timesafari.dailynotification.entities.NotificationConfigEntity
import com.timesafari.dailynotification.dao.NotificationContentDao import org.timesafari.dailynotification.dao.NotificationContentDao
import com.timesafari.dailynotification.dao.NotificationDeliveryDao import org.timesafari.dailynotification.dao.NotificationDeliveryDao
import com.timesafari.dailynotification.dao.NotificationConfigDao import org.timesafari.dailynotification.dao.NotificationConfigDao
/** /**
* Unified SQLite schema for Daily Notification Plugin * Unified SQLite schema for Daily Notification Plugin

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.content.Context; import android.content.Context;

View File

@@ -9,7 +9,7 @@
* @created 2025-10-03 06:53:30 UTC * @created 2025-10-03 06:53:30 UTC
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;

View File

@@ -11,7 +11,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;

View File

@@ -1,4 +1,4 @@
package com.timesafari.dailynotification package org.timesafari.dailynotification
import android.content.Context import android.content.Context
import android.os.SystemClock import android.os.SystemClock
@@ -203,7 +203,7 @@ class FetchWorker(
val notificationId = "notify_$notificationTime" val notificationId = "notify_$notificationTime"
val (title, body) = parsePayload(payload) val (title, body) = parsePayload(payload)
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity( val entity = org.timesafari.dailynotification.entities.NotificationContentEntity(
notificationId, notificationId,
"1.3.3", // Plugin version "1.3.3", // Plugin version
null, // timesafariDid - can be set if available null, // timesafariDid - can be set if available

View File

@@ -15,7 +15,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.List; import java.util.List;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.util.Log; import android.util.Log;
import java.util.UUID; import java.util.UUID;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
@@ -518,13 +518,13 @@ public class NotificationStatusChecker {
* @param database Database instance for querying schedules and history * @param database Database instance for querying schedules and history
* @return JSObject containing notification status (schedules, last notification time, etc.) * @return JSObject containing notification status (schedules, last notification time, etc.)
*/ */
public JSObject getNotificationStatus(com.timesafari.dailynotification.DailyNotificationDatabase database) { public JSObject getNotificationStatus(org.timesafari.dailynotification.DailyNotificationDatabase database) {
try { try {
Log.d(TAG, "DN|NOTIFICATION_STATUS_START"); Log.d(TAG, "DN|NOTIFICATION_STATUS_START");
// Delegate to Kotlin helper function (uses runBlocking internally) // Delegate to Kotlin helper function (uses runBlocking internally)
// This is safe because status checks are quick operations // This is safe because status checks are quick operations
return com.timesafari.dailynotification.NotificationStatusHelper.getNotificationStatusBlocking(database); return org.timesafari.dailynotification.NotificationStatusHelper.getNotificationStatusBlocking(database);
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "DN|NOTIFICATION_STATUS_ERR err=" + e.getMessage(), e); Log.e(TAG, "DN|NOTIFICATION_STATUS_ERR err=" + e.getMessage(), e);

View File

@@ -1,4 +1,4 @@
package com.timesafari.dailynotification package org.timesafari.dailynotification
import android.app.AlarmManager import android.app.AlarmManager
import android.app.AlarmManager.AlarmClockInfo import android.app.AlarmManager.AlarmClockInfo
@@ -148,9 +148,9 @@ class NotifyReceiver : BroadcastReceiver() {
val notificationId = reminderId ?: "notify_${triggerAtMillis}" val notificationId = reminderId ?: "notify_${triggerAtMillis}"
val requestCode = getRequestCode(stableScheduleId) val requestCode = getRequestCode(stableScheduleId)
val checkIntent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { val checkIntent = Intent(context, org.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
setPackage(context.packageName) setPackage(context.packageName)
action = "com.timesafari.daily.NOTIFICATION" action = "org.timesafari.daily.NOTIFICATION"
} }
// IDEMPOTENCE CHECK: Verify no existing alarm for this trigger time before scheduling. // IDEMPOTENCE CHECK: Verify no existing alarm for this trigger time before scheduling.
@@ -254,8 +254,8 @@ class NotifyReceiver : BroadcastReceiver() {
// Always create a notification content entity for recovery tracking // Always create a notification content entity for recovery tracking
// Phase 1: Recovery needs NotificationContentEntity to detect missed notifications // Phase 1: Recovery needs NotificationContentEntity to detect missed notifications
val roomStorage = com.timesafari.dailynotification.storage.DailyNotificationStorageRoom(context) val roomStorage = org.timesafari.dailynotification.storage.DailyNotificationStorageRoom(context)
val entity = com.timesafari.dailynotification.entities.NotificationContentEntity( val entity = org.timesafari.dailynotification.entities.NotificationContentEntity(
notificationId, notificationId,
"1.3.3", // Plugin version "1.3.3", // Plugin version
null, // timesafariDid - can be set if available null, // timesafariDid - can be set if available
@@ -288,9 +288,9 @@ class NotifyReceiver : BroadcastReceiver() {
// FIX: Use DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver // FIX: Use DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver
// FIX: Set action to match manifest registration; setPackage() ensures AlarmManager // FIX: Set action to match manifest registration; setPackage() ensures AlarmManager
// delivery reaches this app on all OEMs (see daily-notification-plugin-android-receiver-issue) // delivery reaches this app on all OEMs (see daily-notification-plugin-android-receiver-issue)
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { val intent = Intent(context, org.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
setPackage(context.packageName) setPackage(context.packageName)
action = "com.timesafari.daily.NOTIFICATION" // Must match manifest intent-filter action action = "org.timesafari.daily.NOTIFICATION" // Must match manifest intent-filter action
putExtra("notification_id", notificationId) // DailyNotificationReceiver expects this extra putExtra("notification_id", notificationId) // DailyNotificationReceiver expects this extra
putExtra("schedule_id", stableScheduleId) // Add stable scheduleId for tracking putExtra("schedule_id", stableScheduleId) // Add stable scheduleId for tracking
// Also preserve original extras for backward compatibility if needed // Also preserve original extras for backward compatibility if needed
@@ -484,9 +484,9 @@ 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 // FIX: Use DailyNotificationReceiver to match what was scheduled
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { val intent = Intent(context, org.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
setPackage(context.packageName) setPackage(context.packageName)
action = "com.timesafari.daily.NOTIFICATION" action = "org.timesafari.daily.NOTIFICATION"
} }
val requestCode = when { val requestCode = when {
scheduleId != null -> getRequestCode(scheduleId) scheduleId != null -> getRequestCode(scheduleId)
@@ -540,9 +540,9 @@ class NotifyReceiver : BroadcastReceiver() {
*/ */
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 // FIX: Use DailyNotificationReceiver to match what was scheduled
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { val intent = Intent(context, org.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
setPackage(context.packageName) setPackage(context.packageName)
action = "com.timesafari.daily.NOTIFICATION" action = "org.timesafari.daily.NOTIFICATION"
} }
val requestCode = when { val requestCode = when {
scheduleId != null -> getRequestCode(scheduleId) scheduleId != null -> getRequestCode(scheduleId)

View File

@@ -1,4 +1,4 @@
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;

View File

@@ -8,7 +8,7 @@
* @version 2.0.0 - Modular Architecture * @version 2.0.0 - Modular Architecture
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.Manifest; import android.Manifest;
import android.content.Context; import android.content.Context;
@@ -87,7 +87,7 @@ public class PermissionManager {
androidx.core.app.ActivityCompat.requestPermissions( androidx.core.app.ActivityCompat.requestPermissions(
activity, activity,
new String[]{android.Manifest.permission.POST_NOTIFICATIONS}, new String[]{android.Manifest.permission.POST_NOTIFICATIONS},
com.timesafari.dailynotification.DailyNotificationConstants.PERMISSION_REQUEST_CODE // Centralized constant org.timesafari.dailynotification.DailyNotificationConstants.PERMISSION_REQUEST_CODE // Centralized constant
); );
Log.d(TAG, "Permission dialog shown, waiting for user response"); Log.d(TAG, "Permission dialog shown, waiting for user response");
@@ -125,7 +125,7 @@ public class PermissionManager {
* *
* @return PermissionStatus with all permission states * @return PermissionStatus with all permission states
*/ */
public com.timesafari.dailynotification.PermissionStatus getPermissionStatus() { public org.timesafari.dailynotification.PermissionStatus getPermissionStatus() {
boolean postNotificationsGranted = false; boolean postNotificationsGranted = false;
boolean exactAlarmsGranted = false; boolean exactAlarmsGranted = false;
boolean notificationsEnabledAtOsLevel = false; boolean notificationsEnabledAtOsLevel = false;
@@ -168,7 +168,7 @@ public class PermissionManager {
batteryOptimizationsIgnored = true; // Pre-Android 6, no battery optimization restrictions batteryOptimizationsIgnored = true; // Pre-Android 6, no battery optimization restrictions
} }
return new com.timesafari.dailynotification.PermissionStatus( return new org.timesafari.dailynotification.PermissionStatus(
postNotificationsGranted, postNotificationsGranted,
exactAlarmsGranted, exactAlarmsGranted,
batteryOptimizationsIgnored, batteryOptimizationsIgnored,
@@ -187,7 +187,7 @@ public class PermissionManager {
try { try {
Log.d(TAG, "Checking permission status"); Log.d(TAG, "Checking permission status");
com.timesafari.dailynotification.PermissionStatus status = getPermissionStatus(); org.timesafari.dailynotification.PermissionStatus status = getPermissionStatus();
JSObject result = status.toJSObject(); JSObject result = status.toJSObject();
result.put("success", true); result.put("success", true);

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification package org.timesafari.dailynotification
/** /**
* Comprehensive permission status model * Comprehensive permission status model

View File

@@ -1,4 +1,4 @@
package com.timesafari.dailynotification package org.timesafari.dailynotification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
@@ -280,7 +280,7 @@ class ReactivationManager(private val context: Context) {
db.notificationContentDao().updateNotification(existing) db.notificationContentDao().updateNotification(existing)
} else { } else {
// Create new notification content entry for missed alarm // Create new notification content entry for missed alarm
val notification = com.timesafari.dailynotification.entities.NotificationContentEntity( val notification = org.timesafari.dailynotification.entities.NotificationContentEntity(
notificationId, notificationId,
"1.3.3", // Plugin version "1.3.3", // Plugin version
null, // timesafariDid null, // timesafariDid
@@ -479,9 +479,9 @@ class ReactivationManager(private val context: Context) {
private fun alarmsExist(): Boolean { private fun alarmsExist(): Boolean {
return try { return try {
// Check if any PendingIntent for our receiver exists (must match NotifyReceiver schedule path) // Check if any PendingIntent for our receiver exists (must match NotifyReceiver schedule path)
val intent = Intent(context, com.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply { val intent = Intent(context, org.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
setPackage(context.packageName) setPackage(context.packageName)
action = "com.timesafari.daily.NOTIFICATION" action = "org.timesafari.daily.NOTIFICATION"
} }
val pendingIntent = PendingIntent.getBroadcast( val pendingIntent = PendingIntent.getBroadcast(
context, context,
@@ -1050,7 +1050,7 @@ class ReactivationManager(private val context: Context) {
db.notificationContentDao().updateNotification(existing) db.notificationContentDao().updateNotification(existing)
} else { } else {
// Create new notification content entry for missed alarm // Create new notification content entry for missed alarm
val notification = com.timesafari.dailynotification.entities.NotificationContentEntity( val notification = org.timesafari.dailynotification.entities.NotificationContentEntity(
notificationId, notificationId,
"1.3.3", // Plugin version "1.3.3", // Plugin version
null, // timesafariDid null, // timesafariDid

View File

@@ -11,7 +11,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;

View File

@@ -8,7 +8,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.os.Trace; import android.os.Trace;

View File

@@ -24,7 +24,7 @@
* @version 1.0.0 * @version 1.0.0
*/ */
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;

View File

@@ -9,10 +9,10 @@
* @since 2025-10-20 * @since 2025-10-20
*/ */
package com.timesafari.dailynotification.dao; package org.timesafari.dailynotification.dao;
import androidx.room.*; import androidx.room.*;
import com.timesafari.dailynotification.entities.NotificationConfigEntity; import org.timesafari.dailynotification.entities.NotificationConfigEntity;
import java.util.List; import java.util.List;

View File

@@ -9,10 +9,10 @@
* @since 2025-10-20 * @since 2025-10-20
*/ */
package com.timesafari.dailynotification.dao; package org.timesafari.dailynotification.dao;
import androidx.room.*; import androidx.room.*;
import com.timesafari.dailynotification.entities.NotificationContentEntity; import org.timesafari.dailynotification.entities.NotificationContentEntity;
import java.util.List; import java.util.List;

View File

@@ -9,10 +9,10 @@
* @since 2025-10-20 * @since 2025-10-20
*/ */
package com.timesafari.dailynotification.dao; package org.timesafari.dailynotification.dao;
import androidx.room.*; import androidx.room.*;
import com.timesafari.dailynotification.entities.NotificationDeliveryEntity; import org.timesafari.dailynotification.entities.NotificationDeliveryEntity;
import java.util.List; import java.util.List;

View File

@@ -9,7 +9,7 @@
* @since 2025-10-20 * @since 2025-10-20
*/ */
package com.timesafari.dailynotification.entities; package org.timesafari.dailynotification.entities;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;

View File

@@ -9,7 +9,7 @@
* @since 2025-10-20 * @since 2025-10-20
*/ */
package com.timesafari.dailynotification.entities; package org.timesafari.dailynotification.entities;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;

View File

@@ -9,7 +9,7 @@
* @since 2025-10-20 * @since 2025-10-20
*/ */
package com.timesafari.dailynotification.entities; package org.timesafari.dailynotification.entities;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;

View File

@@ -9,18 +9,18 @@
* @since 2025-10-20 * @since 2025-10-20
*/ */
package com.timesafari.dailynotification.storage; package org.timesafari.dailynotification.storage;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import com.timesafari.dailynotification.DailyNotificationDatabase; import org.timesafari.dailynotification.DailyNotificationDatabase;
import com.timesafari.dailynotification.dao.NotificationContentDao; import org.timesafari.dailynotification.dao.NotificationContentDao;
import com.timesafari.dailynotification.dao.NotificationDeliveryDao; import org.timesafari.dailynotification.dao.NotificationDeliveryDao;
import com.timesafari.dailynotification.dao.NotificationConfigDao; import org.timesafari.dailynotification.dao.NotificationConfigDao;
import com.timesafari.dailynotification.entities.NotificationContentEntity; import org.timesafari.dailynotification.entities.NotificationContentEntity;
import com.timesafari.dailynotification.entities.NotificationDeliveryEntity; import org.timesafari.dailynotification.entities.NotificationDeliveryEntity;
import com.timesafari.dailynotification.entities.NotificationConfigEntity; import org.timesafari.dailynotification.entities.NotificationConfigEntity;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;

View File

@@ -9,7 +9,7 @@
* @since 2025-12-22 * @since 2025-12-22
*/ */
package com.timesafari.dailynotification package org.timesafari.dailynotification
import android.content.Context import android.content.Context
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider

View File

@@ -12,7 +12,7 @@
* @since 2025-12-22 * @since 2025-12-22
*/ */
package com.timesafari.dailynotification package org.timesafari.dailynotification
import android.content.Context import android.content.Context
import androidx.room.Room import androidx.room.Room

View File

@@ -1,7 +1,7 @@
import { CapacitorConfig } from '@capacitor/cli'; import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = { const config: CapacitorConfig = {
appId: 'com.timesafari.dailynotification', appId: 'org.timesafari.dailynotification',
appName: 'DailyNotification Test App', appName: 'DailyNotification Test App',
webDir: 'www', webDir: 'www',
server: { server: {

View File

@@ -1,6 +1,6 @@
[ [
{ {
"name": "DailyNotification", "name": "DailyNotification",
"class": "com.timesafari.dailynotification.DailyNotificationPlugin" "class": "org.timesafari.dailynotification.DailyNotificationPlugin"
} }
] ]

View File

@@ -38,7 +38,7 @@ pnpm add @timesafari/daily-notification-plugin
```xml ```xml
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.fetch</string> <string>org.timesafari.dailynotification.fetch</string>
</array> </array>
``` ```
@@ -49,7 +49,7 @@ import BackgroundTasks
func application(_ application: UIApplication, func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.timesafari.dailynotification.fetch", BGTaskScheduler.shared.register(forTaskWithIdentifier: "org.timesafari.dailynotification.fetch",
using: nil) { task in using: nil) { task in
// Handle background fetch task // Handle background fetch task
} }

View File

@@ -50,7 +50,7 @@ fi
xcrun simctl install "$SIMULATOR_ID" "$APP_PATH" xcrun simctl install "$SIMULATOR_ID" "$APP_PATH"
# Launch app # Launch app
xcrun simctl launch "$SIMULATOR_ID" com.timesafari.dailynotification.test xcrun simctl launch "$SIMULATOR_ID" org.timesafari.dailynotification.test
``` ```
**Result:** ✅ Simulator now boots and app launches automatically **Result:** ✅ Simulator now boots and app launches automatically

View File

@@ -94,7 +94,7 @@ po UNUserNotificationCenter.current().pendingNotificationRequests()
po await UNUserNotificationCenter.current().notificationSettings() po await UNUserNotificationCenter.current().notificationSettings()
// Manually trigger BGTask (Simulator only) // Manually trigger BGTask (Simulator only)
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"org.timesafari.dailynotification.fetch"]
``` ```
--- ---

View File

@@ -196,7 +196,7 @@ DailyNotificationScheduler: Scheduling notification: [id]
**Solution:** Use simulator-only LLDB command: **Solution:** Use simulator-only LLDB command:
```swift ```swift
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"org.timesafari.dailynotification.fetch"]
``` ```
### Notifications Not Delivering ### Notifications Not Delivering

View File

@@ -169,7 +169,7 @@ The plugin class did NOT conform to `CAPBridgedPlugin` protocol, which is requir
**Solution Implemented (2025-11-13):** **Solution Implemented (2025-11-13):**
1. **Added `CAPBridgedPlugin` conformance** via `@objc` extension: 1. **Added `CAPBridgedPlugin` conformance** via `@objc` extension:
- Implemented `identifier` property (returns `"com.timesafari.dailynotification"`) - Implemented `identifier` property (returns `"org.timesafari.dailynotification"`)
- Implemented `jsName` property (returns `"DailyNotification"`) - Implemented `jsName` property (returns `"DailyNotification"`)
- Implemented `pluginMethods` property (returns array of all `@objc` methods) - Implemented `pluginMethods` property (returns array of all `@objc` methods)
@@ -220,7 +220,7 @@ The plugin class did NOT conform to `CAPBridgedPlugin` protocol, which is requir
- Added diagnostic check to verify class is in `objc_getClassList()` - Added diagnostic check to verify class is in `objc_getClassList()`
2. **Add CAPBridgedPlugin conformance** via `@objc` extension: 2. **Add CAPBridgedPlugin conformance** via `@objc` extension:
- Implemented `identifier` property (returns `"com.timesafari.dailynotification"`) - Implemented `identifier` property (returns `"org.timesafari.dailynotification"`)
- Implemented `jsName` property (returns `"DailyNotification"`) - Implemented `jsName` property (returns `"DailyNotification"`)
- Implemented `pluginMethods` property (returns array of all `@objc` methods) - Implemented `pluginMethods` property (returns array of all `@objc` methods)
@@ -750,7 +750,7 @@ A "successful run" is defined as: BGTask handler invoked, content fetch complete
3. **Serial Queue Pattern (Alternative):** 3. **Serial Queue Pattern (Alternative):**
```swift ```swift
private let stateQueue = DispatchQueue(label: "com.timesafari.dailynotification.state", attributes: .serial) private let stateQueue = DispatchQueue(label: "org.timesafari.dailynotification.state", attributes: .serial)
``` ```
4. **Enforcement:** 4. **Enforcement:**
@@ -1365,8 +1365,8 @@ scripts/
- **Lesson:** Verify actual state, not just command success - **Lesson:** Verify actual state, not just command success
5. **Bundle Identifier Mismatch:** 5. **Bundle Identifier Mismatch:**
- **Issue:** Script was using `com.timesafari.dailynotification.test` but actual bundle ID is `com.timesafari.dailynotification` - **Issue:** Script was using `org.timesafari.dailynotification.test` but actual bundle ID is `org.timesafari.dailynotification`
- **Fix:** Updated all launch commands to use correct bundle ID `com.timesafari.dailynotification` - **Fix:** Updated all launch commands to use correct bundle ID `org.timesafari.dailynotification`
- **Root Cause:** Project file has `.test` suffix but Info.plist resolves to base bundle ID - **Root Cause:** Project file has `.test` suffix but Info.plist resolves to base bundle ID
- **Files Affected:** `scripts/build-ios-test-app.sh` - **Files Affected:** `scripts/build-ios-test-app.sh`
- **Lesson:** Always verify actual bundle ID from installed app, not just project settings; bundle ID resolution can differ from project settings - **Lesson:** Always verify actual bundle ID from installed app, not just project settings; bundle ID resolution can differ from project settings
@@ -1424,7 +1424,7 @@ scripts/
6. **Permission Reset for Testing:** 6. **Permission Reset for Testing:**
- **Issue:** Simulator permissions persist across app launches; need to reset for testing - **Issue:** Simulator permissions persist across app launches; need to reset for testing
- **Fix:** Use `xcrun simctl privacy booted reset all <bundle-id>` to reset permissions - **Fix:** Use `xcrun simctl privacy booted reset all <bundle-id>` to reset permissions
- **Command:** `xcrun simctl privacy booted reset all com.timesafari.dailynotification` - **Command:** `xcrun simctl privacy booted reset all org.timesafari.dailynotification`
- **Lesson:** Simulator permissions don't reset automatically; must manually reset for testing different permission states - **Lesson:** Simulator permissions don't reset automatically; must manually reset for testing different permission states
7. **JavaScript Method Existence Check:** 7. **JavaScript Method Existence Check:**
@@ -1555,7 +1555,7 @@ scripts/
1. **BGTaskScheduler Not Running:** 1. **BGTaskScheduler Not Running:**
- Check Info.plist has `BGTaskSchedulerPermittedIdentifiers` - Check Info.plist has `BGTaskSchedulerPermittedIdentifiers`
- Verify task registered in AppDelegate before app finishes launching - Verify task registered in AppDelegate before app finishes launching
- **Simulator-only debugging trick:** Use LLDB command to manually trigger: `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"]` - **Simulator-only debugging trick:** Use LLDB command to manually trigger: `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"org.timesafari.dailynotification.fetch"]`
- Note: This is for simulator testing only, not available in production - Note: This is for simulator testing only, not available in production
- **Testing Guide:** See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for comprehensive testing procedures - **Testing Guide:** See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for comprehensive testing procedures

View File

@@ -88,7 +88,7 @@ npx cap sync ios
cat android/app/src/main/assets/capacitor.plugins.json | grep DailyNotification cat android/app/src/main/assets/capacitor.plugins.json | grep DailyNotification
# Expected output should include: # Expected output should include:
# "DailyNotification": { "class": "com.timesafari.dailynotification.DailyNotificationPlugin" } # "DailyNotification": { "class": "org.timesafari.dailynotification.DailyNotificationPlugin" }
``` ```
### Error Handling ### Error Handling
@@ -151,14 +151,14 @@ cat android/app/src/main/assets/capacitor.plugins.json | grep DailyNotification
<!-- Daily Notification Plugin Receivers --> <!-- Daily Notification Plugin Receivers -->
<!-- CRITICAL: NotifyReceiver is REQUIRED for notifications to work --> <!-- CRITICAL: NotifyReceiver is REQUIRED for notifications to work -->
<receiver <receiver
android:name="com.timesafari.dailynotification.NotifyReceiver" android:name="org.timesafari.dailynotification.NotifyReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
</receiver> </receiver>
<!-- BootReceiver for reboot recovery (optional but recommended) --> <!-- BootReceiver for reboot recovery (optional but recommended) -->
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
@@ -176,7 +176,7 @@ grep -A 3 "NotifyReceiver" android/app/src/main/AndroidManifest.xml
# Expected output: # Expected output:
# <receiver # <receiver
# android:name="com.timesafari.dailynotification.NotifyReceiver" # android:name="org.timesafari.dailynotification.NotifyReceiver"
# android:enabled="true" # android:enabled="true"
``` ```
@@ -223,8 +223,8 @@ grep -A 3 "NotifyReceiver" android/app/src/main/AndroidManifest.xml
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.content-fetch</string> <string>org.timesafari.dailynotification.content-fetch</string>
<string>com.timesafari.dailynotification.notification-delivery</string> <string>org.timesafari.dailynotification.notification-delivery</string>
</array> </array>
</dict> </dict>
``` ```
@@ -491,7 +491,7 @@ files:
- type: "uses-permission" - type: "uses-permission"
name: "android.permission.POST_NOTIFICATIONS" name: "android.permission.POST_NOTIFICATIONS"
- type: "receiver" - type: "receiver"
name: "com.timesafari.dailynotification.NotifyReceiver" name: "org.timesafari.dailynotification.NotifyReceiver"
attributes: attributes:
android:enabled: "true" android:enabled: "true"
android:exported: "false" android:exported: "false"

View File

@@ -132,7 +132,7 @@ android/plugin/src/main/java/com/timesafari/dailynotification/
### **BootReceiver Registration** ### **BootReceiver Registration**
```xml ```xml
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true" android:exported="true"
android:directBootAware="true"> android:directBootAware="true">

View File

@@ -469,7 +469,7 @@ public class DailyNotificationScheduler {
<!-- Boot Receiver --> <!-- Boot Receiver -->
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true" android:exported="true"
android:directBootAware="true"> android:directBootAware="true">
@@ -482,7 +482,7 @@ public class DailyNotificationScheduler {
<!-- Notification Receiver --> <!-- Notification Receiver -->
<receiver <receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver" android:name="org.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
</receiver> </receiver>

View File

@@ -720,7 +720,7 @@ The plugin **MUST NOT** support or guarantee the following behaviors:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
@@ -730,11 +730,11 @@ The plugin **MUST NOT** support or guarantee the following behaviors:
</receiver> </receiver>
<receiver <receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver" android:name="org.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" /> <action android:name="org.timesafari.daily.NOTIFICATION" />
</intent-filter> </intent-filter>
</receiver> </receiver>
``` ```
@@ -789,15 +789,15 @@ The plugin **MUST NOT** support or guarantee the following behaviors:
#### 10.2.2 Background Tasks #### 10.2.2 Background Tasks
**Required Background Task Identifiers**: **Required Background Task Identifiers**:
* `com.timesafari.dailynotification.fetch` - Background fetch * `org.timesafari.dailynotification.fetch` - Background fetch
* `com.timesafari.dailynotification.notify` - Notification task (if used) * `org.timesafari.dailynotification.notify` - Notification task (if used)
**Background Task Registration**: **Background Task Registration**:
* Register in `Info.plist`: * Register in `Info.plist`:
```xml ```xml
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.fetch</string> <string>org.timesafari.dailynotification.fetch</string>
</array> </array>
``` ```

View File

@@ -16,7 +16,7 @@ This guide provides step-by-step instructions for testing Phase 1 (Cold Start Re
**Environment** **Environment**
- Device: Android Emulator Pixel 8 API 34 - Device: Android Emulator Pixel 8 API 34
- App ID: `com.timesafari.dailynotification` - App ID: `org.timesafari.dailynotification`
- Build: Debug APK from `test-apps/android-test-app` - Build: Debug APK from `test-apps/android-test-app`
- Script: `./test-phase1.sh` - Script: `./test-phase1.sh`
- Date: 27 November 2025 - Date: 27 November 2025
@@ -109,7 +109,7 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk
# Verify installation # Verify installation
adb shell pm list packages | grep timesafari adb shell pm list packages | grep timesafari
# Should show: package:com.timesafari.dailynotification # Should show: package:org.timesafari.dailynotification
``` ```
### Option 2: Vue Test App (More Features) ### Option 2: Vue Test App (More Features)
@@ -162,7 +162,7 @@ adb logcat -s DNP-REACTIVATION > recovery_test.log
```bash ```bash
# Launch app to initialize database # Launch app to initialize database
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Wait a few seconds for initialization # Wait a few seconds for initialization
sleep 3 sleep 3
@@ -181,7 +181,7 @@ sleep 3
adb logcat -c adb logcat -c
# 2. Launch app # 2. Launch app
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# 3. Schedule notification for 2 minutes in future # 3. Schedule notification for 2 minutes in future
# (Use app UI or API - see "Scheduling Notifications" below) # (Use app UI or API - see "Scheduling Notifications" below)
@@ -195,7 +195,7 @@ adb shell dumpsys alarm | grep -i timesafari
# Should show scheduled alarm # Should show scheduled alarm
# 6. Kill app process (simulates OS kill, NOT force stop) # 6. Kill app process (simulates OS kill, NOT force stop)
adb shell am kill com.timesafari.dailynotification adb shell am kill org.timesafari.dailynotification
# 7. Verify app is killed # 7. Verify app is killed
adb shell ps | grep timesafari adb shell ps | grep timesafari
@@ -206,7 +206,7 @@ adb shell ps | grep timesafari
# Or: Set system time forward (see "Time Manipulation" below) # Or: Set system time forward (see "Time Manipulation" below)
# 9. Launch app (cold start) # 9. Launch app (cold start)
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# 10. Check recovery logs immediately # 10. Check recovery logs immediately
adb logcat -d | grep DNP-REACTIVATION adb logcat -d | grep DNP-REACTIVATION
@@ -226,11 +226,11 @@ DNP-REACTIVATION: App launch recovery completed: missed=1, rescheduled=0, verifi
```bash ```bash
# Check database (requires root or debug build) # Check database (requires root or debug build)
adb shell run-as com.timesafari.dailynotification sqlite3 databases/daily_notification_plugin.db \ adb shell run-as org.timesafari.dailynotification sqlite3 databases/daily_notification_plugin.db \
"SELECT id, delivery_status, scheduled_time FROM notification_content WHERE delivery_status = 'missed';" "SELECT id, delivery_status, scheduled_time FROM notification_content WHERE delivery_status = 'missed';"
# Or check history table # Or check history table
adb shell run-as com.timesafari.dailynotification sqlite3 databases/daily_notification_plugin.db \ adb shell run-as org.timesafari.dailynotification sqlite3 databases/daily_notification_plugin.db \
"SELECT * FROM history WHERE kind = 'recovery' ORDER BY occurredAt DESC LIMIT 1;" "SELECT * FROM history WHERE kind = 'recovery' ORDER BY occurredAt DESC LIMIT 1;"
``` ```
@@ -254,7 +254,7 @@ adb shell run-as com.timesafari.dailynotification sqlite3 databases/daily_notifi
adb logcat -c adb logcat -c
# 2. Launch app # 2. Launch app
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# 3. Schedule notification for 10 minutes in future # 3. Schedule notification for 10 minutes in future
# (Use app UI or API) # (Use app UI or API)
@@ -276,7 +276,7 @@ adb shell dumpsys alarm | grep -i timesafari
# Should show no alarms (or fewer alarms) # Should show no alarms (or fewer alarms)
# 7. Launch app (triggers recovery) # 7. Launch app (triggers recovery)
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# 8. Check recovery logs # 8. Check recovery logs
adb logcat -d | grep DNP-REACTIVATION adb logcat -d | grep DNP-REACTIVATION
@@ -318,7 +318,7 @@ adb logcat -c
# See "Database Manipulation" section below # See "Database Manipulation" section below
# 3. Launch app # 3. Launch app
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# 4. Check logs immediately # 4. Check logs immediately
adb logcat -d | grep DNP-REACTIVATION adb logcat -d | grep DNP-REACTIVATION
@@ -354,7 +354,7 @@ adb logcat -c
# See "Database Manipulation" section below # See "Database Manipulation" section below
# 3. Launch app # 3. Launch app
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# 4. Check logs # 4. Check logs
adb logcat -d | grep DNP-REACTIVATION adb logcat -d | grep DNP-REACTIVATION
@@ -426,10 +426,10 @@ adb shell date -s "2025-11-15 14:30:00"
```bash ```bash
# Check if app is debuggable # Check if app is debuggable
adb shell dumpsys package com.timesafari.dailynotification | grep debuggable adb shell dumpsys package org.timesafari.dailynotification | grep debuggable
# Access database # Access database
adb shell run-as com.timesafari.dailynotification sqlite3 databases/daily_notification_plugin.db adb shell run-as org.timesafari.dailynotification sqlite3 databases/daily_notification_plugin.db
# Example: Insert test notification # Example: Insert test notification
sqlite> INSERT INTO notification_content ( sqlite> INSERT INTO notification_content (
@@ -492,7 +492,7 @@ adb logcat -d > phase1_test_$(date +%Y%m%d_%H%M%S).log
#!/bin/bash #!/bin/bash
# Phase 1 Complete Test Sequence # Phase 1 Complete Test Sequence
PACKAGE="com.timesafari.dailynotification" PACKAGE="org.timesafari.dailynotification"
ACTIVITY="${PACKAGE}/.MainActivity" ACTIVITY="${PACKAGE}/.MainActivity"
echo "=== Phase 1 Testing on Emulator ===" echo "=== Phase 1 Testing on Emulator ==="
@@ -601,7 +601,7 @@ adb devices
**Permission denied for database access**: **Permission denied for database access**:
```bash ```bash
# Check if app is debuggable # Check if app is debuggable
adb shell dumpsys package com.timesafari.dailynotification | grep debuggable adb shell dumpsys package org.timesafari.dailynotification | grep debuggable
# If not debuggable, rebuild with debug signing # If not debuggable, rebuild with debug signing
cd test-apps/android-test-app cd test-apps/android-test-app
@@ -617,7 +617,7 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell pm list packages | grep timesafari adb shell pm list packages | grep timesafari
# Uninstall and reinstall # Uninstall and reinstall
adb uninstall com.timesafari.dailynotification adb uninstall org.timesafari.dailynotification
adb install -r app/build/outputs/apk/debug/app-debug.apk adb install -r app/build/outputs/apk/debug/app-debug.apk
``` ```
@@ -658,10 +658,10 @@ cd test-apps/android-test-app
adb install -r app/build/outputs/apk/debug/app-debug.apk adb install -r app/build/outputs/apk/debug/app-debug.apk
# Launch app # Launch app
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Kill app # Kill app
adb shell am kill com.timesafari.dailynotification adb shell am kill org.timesafari.dailynotification
# Monitor logs # Monitor logs
adb logcat -s DNP-REACTIVATION adb logcat -s DNP-REACTIVATION

View File

@@ -90,19 +90,19 @@ Verify that when a force stop clears alarms, the plugin:
3. **Verify alarms are scheduled** 3. **Verify alarms are scheduled**
* Script runs: * Script runs:
```bash ```bash
adb shell dumpsys alarm | grep com.timesafari.dailynotification adb shell dumpsys alarm | grep org.timesafari.dailynotification
``` ```
* Confirm at least one `RTC_WAKEUP` alarm for `com.timesafari.dailynotification`. * Confirm at least one `RTC_WAKEUP` alarm for `org.timesafari.dailynotification`.
4. **Force stop the app** 4. **Force stop the app**
* Script executes: * Script executes:
```bash ```bash
adb shell am force-stop com.timesafari.dailynotification adb shell am force-stop org.timesafari.dailynotification
``` ```
5. **Confirm alarms after force stop** 5. **Confirm alarms after force stop**
* Script re-runs `dumpsys alarm`. * Script re-runs `dumpsys alarm`.
* Ideal test case: **0** alarms for `com.timesafari.dailynotification` (alarms cleared). * Ideal test case: **0** alarms for `org.timesafari.dailynotification` (alarms cleared).
6. **Trigger recovery** 6. **Trigger recovery**
* Script clears logcat and launches the app. * Script clears logcat and launches the app.
@@ -165,12 +165,12 @@ Ensure we **do not run heavy force-stop recovery** when alarms are still intact.
* Click **Test Notification** again to create a second schedule. * Click **Test Notification** again to create a second schedule.
3. **Verify alarms are scheduled** 3. **Verify alarms are scheduled**
* Confirm multiple alarms for `com.timesafari.dailynotification` via `dumpsys alarm`. * Confirm multiple alarms for `org.timesafari.dailynotification` via `dumpsys alarm`.
4. **Simulate a "soft stop"** 4. **Simulate a "soft stop"**
* Script runs: * Script runs:
```bash ```bash
adb shell am kill com.timesafari.dailynotification adb shell am kill org.timesafari.dailynotification
``` ```
* Intent: stop the process but **not** clear alarms (actual behavior may vary by OS). * Intent: stop the process but **not** clear alarms (actual behavior may vary by OS).
@@ -217,7 +217,7 @@ Ensure **force-stop recovery is not mis-triggered** when the app is freshly inst
1. **Clear state** 1. **Clear state**
* Script uninstalls the app to clear DB/state: * Script uninstalls the app to clear DB/state:
```bash ```bash
adb uninstall com.timesafari.dailynotification adb uninstall org.timesafari.dailynotification
``` ```
2. **Reinstall APK** 2. **Reinstall APK**
@@ -273,7 +273,7 @@ Fill this in after your first successful emulator run.
**Environment** **Environment**
- Device: Pixel 8 API 34 (Android 14) - Device: Pixel 8 API 34 (Android 14)
- App ID: `com.timesafari.dailynotification` - App ID: `org.timesafari.dailynotification`
- Build: Debug APK (`app-debug.apk`) from commit `<GIT_HASH>` - Build: Debug APK (`app-debug.apk`) from commit `<GIT_HASH>`
- Script: `./test-phase2.sh` - Script: `./test-phase2.sh`
- Date: 2025-11-XX - Date: 2025-11-XX

View File

@@ -1,7 +1,7 @@
# Phase 2 Force Stop Recovery Verification # Phase 2 Force Stop Recovery Verification
**Plugin:** Daily Notification Plugin **Plugin:** Daily Notification Plugin
**Scope:** Force stop detection & recovery (App ID: `com.timesafari.dailynotification`) **Scope:** Force stop detection & recovery (App ID: `org.timesafari.dailynotification`)
**Related Docs:** **Related Docs:**
- `android-implementation-directive-phase2.md` - `android-implementation-directive-phase2.md`
@@ -142,7 +142,7 @@ or:
**Environment** **Environment**
* Device: Pixel 8 API 34 (Android 14) * Device: Pixel 8 API 34 (Android 14)
* App ID: `com.timesafari.dailynotification` * App ID: `org.timesafari.dailynotification`
* Build: Debug `app-debug.apk` from commit `<GIT_HASH>` * Build: Debug `app-debug.apk` from commit `<GIT_HASH>`
* Script: `./test-phase2.sh` * Script: `./test-phase2.sh`
* Date: 2025-11-XX * Date: 2025-11-XX

View File

@@ -91,7 +91,7 @@ Verify alarms are recreated on boot when schedules have **future run times**.
3. **Verify alarms are scheduled (pre-boot)** 3. **Verify alarms are scheduled (pre-boot)**
* Script calls `show_alarms` and `count_alarms`. * Script calls `show_alarms` and `count_alarms`.
* You should see at least one `RTC_WAKEUP` entry for `com.timesafari.dailynotification`. * You should see at least one `RTC_WAKEUP` entry for `org.timesafari.dailynotification`.
4. **Reboot emulator** 4. **Reboot emulator**
* Script calls `reboot_emulator`: * Script calls `reboot_emulator`:
@@ -193,7 +193,7 @@ Verify boot recovery handles an **empty DB / no schedules** safely and does **no
1. **Uninstall app to clear DB/state** 1. **Uninstall app to clear DB/state**
* Script calls: * Script calls:
```bash ```bash
adb uninstall com.timesafari.dailynotification adb uninstall org.timesafari.dailynotification
``` ```
2. **Reinstall APK** 2. **Reinstall APK**

View File

@@ -146,7 +146,7 @@ Script passes if:
**Environment** **Environment**
* Device: Pixel 8 API 34 (Android 14) * Device: Pixel 8 API 34 (Android 14)
* App ID: `com.timesafari.dailynotification` * App ID: `org.timesafari.dailynotification`
* Build: Debug `app-debug.apk` from commit `<GIT_HASH>` * Build: Debug `app-debug.apk` from commit `<GIT_HASH>`
* Script: `./test-phase3.sh` * Script: `./test-phase3.sh`
* Date: 2025-11-XX * Date: 2025-11-XX

View File

@@ -223,8 +223,8 @@ public class MyNativeFetcher implements NativeNotificationContentFetcher {
package com.example.app; package com.example.app;
import android.app.Application; import android.app.Application;
import com.timesafari.dailynotification.DailyNotificationPlugin; import org.timesafari.dailynotification.DailyNotificationPlugin;
import com.timesafari.dailynotification.NativeNotificationContentFetcher; import org.timesafari.dailynotification.NativeNotificationContentFetcher;
public class MyApplication extends Application { public class MyApplication extends Application {
@Override @Override
@@ -285,7 +285,7 @@ export async function setupDailyNotifications() {
package com.example.app; package com.example.app;
import android.util.Log; import android.util.Log;
import com.timesafari.dailynotification.*; import org.timesafari.dailynotification.*;
import java.util.*; import java.util.*;
public class MyNativeFetcher implements NativeNotificationContentFetcher { public class MyNativeFetcher implements NativeNotificationContentFetcher {

View File

@@ -18,8 +18,8 @@ This document provides comprehensive guidance for legal and store compliance req
const iosBackgroundTaskConfig = { const iosBackgroundTaskConfig = {
// Required: Register background task identifiers // Required: Register background task identifiers
backgroundTaskIdentifiers: [ backgroundTaskIdentifiers: [
'com.timesafari.dailynotification.fetch', 'org.timesafari.dailynotification.fetch',
'com.timesafari.dailynotification.maintenance' 'org.timesafari.dailynotification.maintenance'
], ],
// Required: Background modes in Info.plist // Required: Background modes in Info.plist

View File

@@ -794,7 +794,7 @@ class SecureJWTStorage(private val context: Context) {
**iOS (iOS Keychain)**: **iOS (iOS Keychain)**:
```swift ```swift
class SecureJWTStorage { class SecureJWTStorage {
private let keychain = Keychain(service: "com.timesafari.dailynotification") private let keychain = Keychain(service: "org.timesafari.dailynotification")
func storeJWTSecret(_ secret: String) throws { func storeJWTSecret(_ secret: String) throws {
let data = secret.data(using: .utf8)! let data = secret.data(using: .utf8)!
@@ -1006,7 +1006,7 @@ fun scheduleImmediateCatchUp() {
**iOS (BGTaskScheduler)**: **iOS (BGTaskScheduler)**:
```swift ```swift
// BGTaskScheduler Configuration // BGTaskScheduler Configuration
let taskIdentifier = "com.timesafari.dailynotification.starred-projects-polling" let taskIdentifier = "org.timesafari.dailynotification.starred-projects-polling"
// Register background task // Register background task
BGTaskScheduler.shared.register( BGTaskScheduler.shared.register(
@@ -1025,7 +1025,7 @@ try BGTaskScheduler.shared.submit(request)
/* /*
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.starred-projects-polling</string> <string>org.timesafari.dailynotification.starred-projects-polling</string>
</array> </array>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
@@ -2472,7 +2472,7 @@ interface PollingScheduleConfig<TRequest, TResponse> {
**File**: `src/android/GenericPollingManager.java` **File**: `src/android/GenericPollingManager.java`
```java ```java
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;

View File

@@ -19,7 +19,7 @@ The initial approach using `BootReceiver` to restore notifications after device
```bash ```bash
# Boot receiver was registered but not triggered # Boot receiver was registered but not triggered
adb shell "dumpsys package com.timesafari.dailynotification | grep -A5 -B5 BootReceiver" adb shell "dumpsys package org.timesafari.dailynotification | grep -A5 -B5 BootReceiver"
# Output: BootReceiver registered but not in enabledComponents list # Output: BootReceiver registered but not in enabledComponents list
# After reboot, no recovery logs appeared # After reboot, no recovery logs appeared
@@ -167,7 +167,7 @@ adb shell "dumpsys alarm | grep timesafari"
```bash ```bash
# 1. Schedule notification # 1. Schedule notification
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Tap "Test Notification" (5 minutes from now) # Tap "Test Notification" (5 minutes from now)
# 2. Verify initial scheduling # 2. Verify initial scheduling
@@ -179,7 +179,7 @@ adb reboot
# Wait 2-3 minutes for boot completion # Wait 2-3 minutes for boot completion
# 4. Launch app (triggers recovery) # 4. Launch app (triggers recovery)
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# 5. Check recovery logs # 5. Check recovery logs
adb logcat -d | grep -i "recovery" | tail -5 adb logcat -d | grep -i "recovery" | tail -5

View File

@@ -621,7 +621,7 @@ Gaps uncovered:
adb shell dumpsys alarm | grep -i timesafari adb shell dumpsys alarm | grep -i timesafari
# Force kill (not force-stop) - adjust package name based on test app # Force kill (not force-stop) - adjust package name based on test app
adb shell am kill com.timesafari.dailynotification adb shell am kill org.timesafari.dailynotification
# Or for test apps: # Or for test apps:
# adb shell am kill com.timesafari.androidtestapp # adb shell am kill com.timesafari.androidtestapp
# adb shell am kill <package-name-from-test-app-manifest> # adb shell am kill <package-name-from-test-app-manifest>

View File

@@ -305,7 +305,7 @@ The plugin **must**:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
@@ -315,11 +315,11 @@ The plugin **must**:
</receiver> </receiver>
<receiver <receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver" android:name="org.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" /> <action android:name="org.timesafari.daily.NOTIFICATION" />
</intent-filter> </intent-filter>
</receiver> </receiver>
``` ```
@@ -372,15 +372,15 @@ The plugin **must**:
#### 5.2.2 Background Tasks #### 5.2.2 Background Tasks
**Required Background Task Identifiers**: **Required Background Task Identifiers**:
* `com.timesafari.dailynotification.fetch` - Background fetch * `org.timesafari.dailynotification.fetch` - Background fetch
* `com.timesafari.dailynotification.notify` - Notification task (if used) * `org.timesafari.dailynotification.notify` - Notification task (if used)
**Background Task Registration**: **Background Task Registration**:
* Register in `Info.plist`: * Register in `Info.plist`:
```xml ```xml
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.fetch</string> <string>org.timesafari.dailynotification.fetch</string>
</array> </array>
``` ```

View File

@@ -39,7 +39,7 @@ Add to `Info.plist`:
```xml ```xml
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.fetch</string> <string>org.timesafari.dailynotification.fetch</string>
</array> </array>
``` ```

View File

@@ -0,0 +1,129 @@
# Consuming App Migration: com.timesafari → org.timesafari
Use this document in your **consuming app** repo (e.g. with Cursor or as a checklist) to migrate from `com.timesafari.dailynotification` to `org.timesafari.dailynotification` after the daily-notification-plugin has been updated.
## Summary of plugin changes
The plugins package/namespace and public identifiers were renamed:
- **Package/namespace**: `com.timesafari.dailynotification``org.timesafari.dailynotification`
- **Intent action (Android)**: `com.timesafari.daily.NOTIFICATION``org.timesafari.daily.NOTIFICATION`
- **Dismiss action (Android)**: `com.timesafari.daily.DISMISS``org.timesafari.daily.DISMISS`
- **iOS BGTask identifiers**: `com.timesafari.dailynotification.fetch` / `.notify` / `.prefetch``org.timesafari.dailynotification.*`
- **Capacitor plugin class**: `com.timesafari.dailynotification.DailyNotificationPlugin``org.timesafari.dailynotification.DailyNotificationPlugin`
Your app must align with these so the plugin loads and receivers/intents work.
---
## 1. Update plugin dependency
- Ensure the consuming app depends on a **version of the plugin that already uses `org.timesafari`** (e.g. after the plugin repos com→org migration is merged/released).
- Reinstall/sync: `npm install` (or equivalent) so the updated plugin is used.
---
## 2. Capacitor plugin registration (Android & iOS)
The plugin is registered by **class name**. Update every place that references the plugin class.
**Typical locations:**
- `android/app/src/main/assets/capacitor.plugins.json` (or `public/plugins` if you use that)
- Any script or config that writes the plugin class string
**Change:**
- From: `"class": "com.timesafari.dailynotification.DailyNotificationPlugin"` or `"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"`
- To: `"class": "org.timesafari.dailynotification.DailyNotificationPlugin"` or `"classpath": "org.timesafari.dailynotification.DailyNotificationPlugin"`
If you use a script (e.g. `fix-capacitor-plugins.js`) that injects this class name, update the script to use `org.timesafari.dailynotification.DailyNotificationPlugin`.
---
## 3. Android: `AndroidManifest.xml`
**Receiver class names** (must match the plugins new package):
- `com.timesafari.dailynotification.DailyNotificationReceiver``org.timesafari.dailynotification.DailyNotificationReceiver`
- `com.timesafari.dailynotification.NotifyReceiver``org.timesafari.dailynotification.NotifyReceiver`
- `com.timesafari.dailynotification.BootReceiver``org.timesafari.dailynotification.BootReceiver`
- Any other `com.timesafari.dailynotification.*` receiver → `org.timesafari.dailynotification.*`
**Intent action** (must match plugins `DailyNotificationConstants.ACTION_NOTIFICATION`):
- `<action android:name="com.timesafari.daily.NOTIFICATION" />``<action android:name="org.timesafari.daily.NOTIFICATION" />`
Search the manifest for `com.timesafari` and replace with `org.timesafari` for these plugin-related entries.
---
## 4. Android: ProGuard / R8 (if you keep plugin classes)
If you have custom keep rules that reference the plugin package:
- `com.timesafari.dailynotification``org.timesafari.dailynotification`
(e.g. `-keep class com.timesafari.dailynotification.**``-keep class org.timesafari.dailynotification.**`).
---
## 5. iOS: `Info.plist` (BGTask identifiers)
The plugin uses these background task identifiers. Your apps `Info.plist` must list the **same** identifiers.
**In `BGTaskSchedulerPermittedIdentifiers` (or equivalent):**
- `com.timesafari.dailynotification.fetch``org.timesafari.dailynotification.fetch`
- `com.timesafari.dailynotification.notify``org.timesafari.dailynotification.notify`
If you use the prefetch identifier:
- `com.timesafari.dailynotification.prefetch``org.timesafari.dailynotification.prefetch`
Update any other `com.timesafari.dailynotification.*` task IDs to `org.timesafari.dailynotification.*`.
---
## 6. App ID / Bundle ID (optional; breaking for installs)
- **Plugin package/namespace** change does **not** require you to change your apps **applicationId** (Android) or **Bundle ID** (iOS).
- If you **do** change your apps id from `com.timesafari.*` to `org.timesafari.*`, the store and OS will treat it as a **new app** (new install, no in-place update). Only change this if you intend that.
---
## 7. Custom intent actions / deep links
If your app or backend uses custom actions that included the old prefix (e.g. `com.timesafari.dailynotification.REFRESH_DATA` or `OPEN_SETTINGS`), update them to `org.timesafari.dailynotification.*` so they still match what the plugin or your code expects.
---
## 8. Scripts and docs in the consuming app
- Any script that uses the **plugin package** or **intent action** (e.g. `adb shell am start`, `dumpsys package`, or broadcast actions) should use `org.timesafari.dailynotification` and `org.timesafari.daily.NOTIFICATION` where applicable.
- Update internal docs or runbooks that reference the old package or action strings.
---
## 9. Verification
- **Android**: Build the app, install, and confirm notifications still schedule and fire. Check `adb shell dumpsys package <your.package>` for receivers and that alarms use `org.timesafari.daily.NOTIFICATION` if you inspect with `dumpsys alarm`.
- **iOS**: Build and run; confirm BGTask registration and notification behavior. Ensure `Info.plist` identifiers match the plugins Swift constants.
- **Capacitor**: Confirm the plugin is loaded (e.g. no “class not found” or missing plugin in the bridge).
---
## Quick find-and-replace (consuming app only)
Use with care; prefer updating specific files as above. Suggested patterns:
- **Plugin class**: `com.timesafari.dailynotification.DailyNotificationPlugin``org.timesafari.dailynotification.DailyNotificationPlugin`
- **Receiver/package references**: `com.timesafari.dailynotification.``org.timesafari.dailynotification.`
- **Notification intent action**: `com.timesafari.daily.NOTIFICATION``org.timesafari.daily.NOTIFICATION`
- **BGTask identifiers**: `com.timesafari.dailynotification.fetch` / `.notify` / `.prefetch``org.timesafari.dailynotification.fetch` / `.notify` / `.prefetch`
Do **not** blindly replace `com.timesafari` in your **apps own** applicationId/Bundle ID unless you intend to ship as a new app.
---
**Reference:** This migration is aligned with the changes in the `daily-notification-plugin` repo (package rename com → org). For plugin-side details, see that repos history and docs.

View File

@@ -508,10 +508,10 @@ Add required permissions to `android/app/src/main/AndroidManifest.xml`:
<!-- Existing application configuration --> <!-- Existing application configuration -->
<!-- Daily Notification Plugin receivers and services --> <!-- Daily Notification Plugin receivers and services -->
<receiver android:name="com.timesafari.dailynotification.NotifyReceiver" <receiver android:name="org.timesafari.dailynotification.NotifyReceiver"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<receiver android:name="com.timesafari.dailynotification.BootReceiver" <receiver android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
@@ -599,8 +599,8 @@ Add required permissions to `ios/App/App/Info.plist`:
<!-- BGTaskScheduler identifiers --> <!-- BGTaskScheduler identifiers -->
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.content-fetch</string> <string>org.timesafari.dailynotification.content-fetch</string>
<string>com.timesafari.dailynotification.notification-delivery</string> <string>org.timesafari.dailynotification.notification-delivery</string>
</array> </array>
<!-- Notification usage description --> <!-- Notification usage description -->

View File

@@ -59,14 +59,14 @@ Add to `android/app/src/main/AndroidManifest.xml`:
<!-- Daily Notification Plugin Receivers --> <!-- Daily Notification Plugin Receivers -->
<!-- REQUIRED: NotifyReceiver for AlarmManager-based notifications --> <!-- REQUIRED: NotifyReceiver for AlarmManager-based notifications -->
<receiver <receiver
android:name="com.timesafari.dailynotification.NotifyReceiver" android:name="org.timesafari.dailynotification.NotifyReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
</receiver> </receiver>
<!-- BootReceiver for reboot recovery (optional but recommended) --> <!-- BootReceiver for reboot recovery (optional but recommended) -->
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
@@ -108,8 +108,8 @@ Add to `ios/App/App/Info.plist`:
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.content-fetch</string> <string>org.timesafari.dailynotification.content-fetch</string>
<string>com.timesafari.dailynotification.notification-delivery</string> <string>org.timesafari.dailynotification.notification-delivery</string>
</array> </array>
``` ```

View File

@@ -77,7 +77,7 @@ DailyNotificationPlugin.load()
**File to Create**: `NativeNotificationContentFetcher.java` **File to Create**: `NativeNotificationContentFetcher.java`
```java ```java
package com.timesafari.dailynotification; package org.timesafari.dailynotification;
public interface NativeNotificationContentFetcher { public interface NativeNotificationContentFetcher {
java.util.concurrent.CompletableFuture<java.util.List<NotificationContent>> java.util.concurrent.CompletableFuture<java.util.List<NotificationContent>>

View File

@@ -24,7 +24,7 @@ This document provides comprehensive troubleshooting guidance for integrating th
**Error Message:** **Error Message:**
``` ```
Duplicate class com.timesafari.dailynotification.BootReceiver found in modules: Duplicate class org.timesafari.dailynotification.BootReceiver found in modules:
- plugin-debug.aar -> plugin-debug-runtime (:plugin-debug:) - plugin-debug.aar -> plugin-debug-runtime (:plugin-debug:)
- plugin-debug.aar -> plugin-debug-runtime (plugin-debug.aar) - plugin-debug.aar -> plugin-debug-runtime (plugin-debug.aar)
``` ```
@@ -82,7 +82,7 @@ implementation(name: 'plugin-debug', ext: 'aar')
{ {
"id": "DailyNotification", "id": "DailyNotification",
"name": "DailyNotification", "name": "DailyNotification",
"class": "com.timesafari.dailynotification.DailyNotificationPlugin" "class": "org.timesafari.dailynotification.DailyNotificationPlugin"
} }
``` ```
@@ -166,7 +166,7 @@ dependencies {
{ {
"id": "DailyNotification", "id": "DailyNotification",
"name": "DailyNotification", "name": "DailyNotification",
"class": "com.timesafari.dailynotification.DailyNotificationPlugin" "class": "org.timesafari.dailynotification.DailyNotificationPlugin"
} }
] ]
``` ```

View File

@@ -180,8 +180,8 @@ await DailyNotification.configure({
iosConfig: { iosConfig: {
// Background task configuration // Background task configuration
backgroundTasks: { backgroundTasks: {
'com.timesafari.daily-notification-fetch': { 'org.timesafari.daily-notification-fetch': {
identifier: 'com.timesafari.daily-notification-fetch', identifier: 'org.timesafari.daily-notification-fetch',
requiresNetworkConnectivity: true, requiresNetworkConnectivity: true,
requiresExternalPower: false, requiresExternalPower: false,
requiresDeviceIdle: false requiresDeviceIdle: false

View File

@@ -118,7 +118,7 @@ public class MainActivity extends BridgeActivity {
```xml ```xml
<!-- App Configuration --> <!-- App Configuration -->
<application android:name="com.timesafari.dailynotification"> <application android:name="org.timesafari.dailynotification">
<activity android:name=".MainActivity" android:exported="true"> <activity android:name=".MainActivity" android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -142,16 +142,16 @@ public class MainActivity extends BridgeActivity {
<!-- Plugin Components --> <!-- Plugin Components -->
<!-- Internal receiver: keep non-exported unless intentionally public --> <!-- Internal receiver: keep non-exported unless intentionally public -->
<receiver <receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver" android:name="org.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" /> <action android:name="org.timesafari.daily.NOTIFICATION" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true"> android:exported="true">
<intent-filter android:priority="1000"> <intent-filter android:priority="1000">
@@ -164,7 +164,7 @@ public class MainActivity extends BridgeActivity {
**Minimal example (recommended):** **Minimal example (recommended):**
```xml ```xml
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
@@ -203,7 +203,7 @@ public class MainActivity extends BridgeActivity {
```json ```json
{ {
"appId": "com.timesafari.dailynotification", "appId": "org.timesafari.dailynotification",
"appName": "DailyNotification Test App", "appName": "DailyNotification Test App",
"webDir": "www", "webDir": "www",
"server": { "server": {
@@ -229,7 +229,7 @@ public class MainActivity extends BridgeActivity {
[ [
{ {
"name": "DailyNotification", "name": "DailyNotification",
"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin" "classpath": "org.timesafari.dailynotification.DailyNotificationPlugin"
} }
] ]
``` ```
@@ -429,11 +429,11 @@ dependencies {
```gradle ```gradle
android { android {
namespace "com.timesafari.dailynotification" namespace "org.timesafari.dailynotification"
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig { defaultConfig {
applicationId "com.timesafari.dailynotification" applicationId "org.timesafari.dailynotification"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1

View File

@@ -110,7 +110,7 @@ www/
#### Implementation Plan #### Implementation Plan
```java ```java
// New organization // New organization
com.timesafari.dailynotification/ org.timesafari.dailynotification/
plugin/ plugin/
DailyNotificationPlugin.java (thin facade) DailyNotificationPlugin.java (thin facade)
usecases/ usecases/
@@ -587,16 +587,16 @@ public class SecureNetworkClient {
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<receiver <receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver" android:name="org.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" /> <action android:name="org.timesafari.daily.NOTIFICATION" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true"> android:exported="true">
<intent-filter android:priority="1000"> <intent-filter android:priority="1000">
@@ -605,7 +605,7 @@ public class SecureNetworkClient {
</receiver> </receiver>
<receiver <receiver
android:name="com.timesafari.dailynotification.TimeChangeReceiver" android:name="org.timesafari.dailynotification.TimeChangeReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>

View File

@@ -96,7 +96,7 @@ This directive provides **descriptive overview and integration guidance** for An
**⚠️ Illustrative only** See Phase 1 for canonical implementation. **⚠️ Illustrative only** See Phase 1 for canonical implementation.
```kotlin ```kotlin
package com.timesafari.dailynotification package org.timesafari.dailynotification
import android.app.AlarmManager import android.app.AlarmManager
import android.content.Context import android.content.Context
@@ -763,7 +763,7 @@ private suspend fun handleMissedAlarmOnBoot(
### 5.1 Test Setup ### 5.1 Test Setup
**Package Name**: `com.timesafari.dailynotification` **Package Name**: `org.timesafari.dailynotification`
**Test App Location**: `test-apps/android-test-app/` **Test App Location**: `test-apps/android-test-app/`
@@ -806,7 +806,7 @@ adb shell dumpsys jobscheduler | grep -i timesafari
#### Step 1: Schedule Alarm #### Step 1: Schedule Alarm
**Via Test App UI**: **Via Test App UI**:
1. Launch test app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity` 1. Launch test app: `adb shell am start -n org.timesafari.dailynotification/.MainActivity`
2. Click "Test Notification" button 2. Click "Test Notification" button
3. Schedule alarm for 4 minutes in future (test app default) 3. Schedule alarm for 4 minutes in future (test app default)
4. Note the scheduled time 4. Note the scheduled time
@@ -814,7 +814,7 @@ adb shell dumpsys jobscheduler | grep -i timesafari
**Via ADB (Alternative)**: **Via ADB (Alternative)**:
```bash ```bash
# Launch app # Launch app
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Wait for app to load, then use UI to schedule # Wait for app to load, then use UI to schedule
# Or use monkey to click button (if button ID known) # Or use monkey to click button (if button ID known)
@@ -835,7 +835,7 @@ adb logcat -d | grep "DN|SCHEDULE\|DN|ALARM"
```bash ```bash
# Kill app process (simulates OS kill) # Kill app process (simulates OS kill)
adb shell am kill com.timesafari.dailynotification adb shell am kill org.timesafari.dailynotification
# Verify app is killed # Verify app is killed
adb shell ps | grep timesafari adb shell ps | grep timesafari
@@ -866,7 +866,7 @@ adb shell date
```bash ```bash
# Launch app (triggers cold start) # Launch app (triggers cold start)
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Monitor logs for recovery # Monitor logs for recovery
adb logcat -c # Clear logs first adb logcat -c # Clear logs first
@@ -915,7 +915,7 @@ adb logcat -d | grep -E "DNP-REACTIVATION|COLD_START|missed"
#### Step 1: Schedule Multiple Alarms #### Step 1: Schedule Multiple Alarms
**Via Test App UI**: **Via Test App UI**:
1. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity` 1. Launch app: `adb shell am start -n org.timesafari.dailynotification/.MainActivity`
2. Schedule alarm #1 for 2 minutes in future 2. Schedule alarm #1 for 2 minutes in future
3. Schedule alarm #2 for 5 minutes in future 3. Schedule alarm #2 for 5 minutes in future
4. Schedule alarm #3 for 10 minutes in future 4. Schedule alarm #3 for 10 minutes in future
@@ -936,7 +936,7 @@ adb logcat -d | grep "DN|SCHEDULE"
```bash ```bash
# Force stop app (hard kill) # Force stop app (hard kill)
adb shell am force-stop com.timesafari.dailynotification adb shell am force-stop org.timesafari.dailynotification
# Verify app is force-stopped # Verify app is force-stopped
adb shell ps | grep timesafari adb shell ps | grep timesafari
@@ -963,7 +963,7 @@ adb shell date
```bash ```bash
# Launch app (triggers force stop recovery) # Launch app (triggers force stop recovery)
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Monitor logs immediately # Monitor logs immediately
adb logcat -c adb logcat -c
@@ -1014,7 +1014,7 @@ adb shell dumpsys alarm | grep -A 10 timesafari
#### Step 1: Schedule Alarm Before Reboot #### Step 1: Schedule Alarm Before Reboot
**Via Test App UI**: **Via Test App UI**:
1. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity` 1. Launch app: `adb shell am start -n org.timesafari.dailynotification/.MainActivity`
2. Schedule alarm for 5 minutes in future 2. Schedule alarm for 5 minutes in future
3. Note scheduled time 3. Note scheduled time
@@ -1075,7 +1075,7 @@ adb logcat -d | grep -E "DNP-BOOT|BOOT_COMPLETED|reschedule"
```bash ```bash
# Launch app to verify state # Launch app to verify state
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Check if missed alarms were handled # Check if missed alarms were handled
adb logcat -d | grep -E "missed|boot_recovery" adb logcat -d | grep -E "missed|boot_recovery"
@@ -1103,7 +1103,7 @@ adb logcat -d | grep -E "missed|boot_recovery"
#### Step 1: Schedule Alarm #### Step 1: Schedule Alarm
**Via Test App UI**: **Via Test App UI**:
1. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity` 1. Launch app: `adb shell am start -n org.timesafari.dailynotification/.MainActivity`
2. Schedule alarm for 10 minutes in future 2. Schedule alarm for 10 minutes in future
3. Note scheduled time 3. Note scheduled time
@@ -1142,7 +1142,7 @@ adb shell ps | grep timesafari
```bash ```bash
# Bring app to foreground # Bring app to foreground
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Monitor logs for warm start recovery # Monitor logs for warm start recovery
adb logcat -c adb logcat -c
@@ -1191,7 +1191,7 @@ adb shell dumpsys alarm | grep -A 5 timesafari
```bash ```bash
# Launch app and schedule alarm for 4 minutes # Launch app and schedule alarm for 4 minutes
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Use UI to schedule alarm # Use UI to schedule alarm
``` ```
@@ -1269,7 +1269,7 @@ adb logcat -d | grep -E "DN|RECEIVE_START|DN|WORK_START|DN|DISPLAY"
**Android 12+ (API 31+)**: **Android 12+ (API 31+)**:
```bash ```bash
# Check current permission status # Check current permission status
adb shell dumpsys package com.timesafari.dailynotification | grep -i "schedule_exact_alarm" adb shell dumpsys package org.timesafari.dailynotification | grep -i "schedule_exact_alarm"
# Revoke permission (requires root or system app) # Revoke permission (requires root or system app)
# Or use Settings UI: # Or use Settings UI:
@@ -1299,7 +1299,7 @@ adb logcat -d | grep -E "EXACT_ALARM|permission|SCHEDULE_EXACT"
adb shell am start -a android.settings.REQUEST_SCHEDULE_EXACT_ALARM adb shell am start -a android.settings.REQUEST_SCHEDULE_EXACT_ALARM
# Or navigate manually: # Or navigate manually:
adb shell am start -a android.settings.APPLICATION_DETAILS_SETTINGS -d package:com.timesafari.dailynotification adb shell am start -a android.settings.APPLICATION_DETAILS_SETTINGS -d package:org.timesafari.dailynotification
``` ```
#### Step 4: Verify Alarm Scheduling #### Step 4: Verify Alarm Scheduling
@@ -1412,7 +1412,7 @@ adb shell date +%s
```bash ```bash
#!/bin/bash #!/bin/bash
PACKAGE="com.timesafari.dailynotification" PACKAGE="org.timesafari.dailynotification"
ACTIVITY="${PACKAGE}/.MainActivity" ACTIVITY="${PACKAGE}/.MainActivity"
echo "=== Test 1: Cold Start Recovery ===" echo "=== Test 1: Cold Start Recovery ==="

View File

@@ -84,7 +84,7 @@ Phase 1 implements **minimal viable app launch recovery** for cold start scenari
### 2.2 Class Structure ### 2.2 Class Structure
```kotlin ```kotlin
package com.timesafari.dailynotification package org.timesafari.dailynotification
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
@@ -561,9 +561,9 @@ override fun load() {
**Steps**: **Steps**:
1. Schedule notification for 2 minutes in future 1. Schedule notification for 2 minutes in future
2. Kill app process: `adb shell am kill com.timesafari.dailynotification` 2. Kill app process: `adb shell am kill org.timesafari.dailynotification`
3. Wait 5 minutes (past scheduled time) 3. Wait 5 minutes (past scheduled time)
4. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity` 4. Launch app: `adb shell am start -n org.timesafari.dailynotification/.MainActivity`
5. Check logs: `adb logcat -d | grep DNP-REACTIVATION` 5. Check logs: `adb logcat -d | grep DNP-REACTIVATION`
**Expected**: **Expected**:

View File

@@ -163,7 +163,7 @@ private fun alarmsExist(): Boolean {
// 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, DailyNotificationReceiver::class.java).apply { val intent = Intent(context, DailyNotificationReceiver::class.java).apply {
action = "com.timesafari.daily.NOTIFICATION" action = "org.timesafari.daily.NOTIFICATION"
} }
val pendingIntent = PendingIntent.getBroadcast( val pendingIntent = PendingIntent.getBroadcast(
context, context,
@@ -629,9 +629,9 @@ private fun calculateNextOccurrence(schedule: Schedule, fromTime: Long): Long {
**Steps**: **Steps**:
1. Schedule 3 notifications (2 minutes, 5 minutes, 10 minutes in future) 1. Schedule 3 notifications (2 minutes, 5 minutes, 10 minutes in future)
2. Verify alarms scheduled: `adb shell dumpsys alarm | grep timesafari` 2. Verify alarms scheduled: `adb shell dumpsys alarm | grep timesafari`
3. Force stop app: `adb shell am force-stop com.timesafari.dailynotification` 3. Force stop app: `adb shell am force-stop org.timesafari.dailynotification`
4. Verify alarms cancelled: `adb shell dumpsys alarm | grep timesafari` (should be empty) 4. Verify alarms cancelled: `adb shell dumpsys alarm | grep timesafari` (should be empty)
5. Launch app: `adb shell am start -n com.timesafari.dailynotification/.MainActivity` 5. Launch app: `adb shell am start -n org.timesafari.dailynotification/.MainActivity`
6. Check logs: `adb logcat -d | grep DNP-REACTIVATION` 6. Check logs: `adb logcat -d | grep DNP-REACTIVATION`
**Expected**: **Expected**:

View File

@@ -29,7 +29,7 @@ public class TestApplication extends Application {
Context context = getApplicationContext(); Context context = getApplicationContext();
NativeNotificationContentFetcher testFetcher = NativeNotificationContentFetcher testFetcher =
new com.timesafari.dailynotification.test.TestNativeFetcher(context); new org.timesafari.dailynotification.test.TestNativeFetcher(context);
DailyNotificationPlugin.setNativeFetcher(testFetcher); DailyNotificationPlugin.setNativeFetcher(testFetcher);
} }
} }
@@ -175,7 +175,7 @@ implementation 'com.google.code.gson:gson:2.10.1'
**Test App (Working):** **Test App (Working):**
```xml ```xml
<receiver <receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver" android:name="org.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> <!-- Note: false --> android:exported="false"> <!-- Note: false -->
``` ```
@@ -183,7 +183,7 @@ implementation 'com.google.code.gson:gson:2.10.1'
**TimeSafari (Broken):** **TimeSafari (Broken):**
```xml ```xml
<receiver <receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver" android:name="org.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true" android:enabled="true"
android:exported="true"> <!-- Note: true - potential security issue --> android:exported="true"> <!-- Note: true - potential security issue -->
``` ```
@@ -240,8 +240,8 @@ package app.timesafari;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import com.timesafari.dailynotification.DailyNotificationPlugin; import org.timesafari.dailynotification.DailyNotificationPlugin;
import com.timesafari.dailynotification.NativeNotificationContentFetcher; import org.timesafari.dailynotification.NativeNotificationContentFetcher;
public class TimeSafariApplication extends Application { public class TimeSafariApplication extends Application {
@@ -272,8 +272,8 @@ Create file: `android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.j
package app.timesafari; package app.timesafari;
import android.content.Context; import android.content.Context;
import com.timesafari.dailynotification.NativeNotificationContentFetcher; import org.timesafari.dailynotification.NativeNotificationContentFetcher;
import com.timesafari.dailynotification.NotificationContent; import org.timesafari.dailynotification.NotificationContent;
public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher { public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher {
@@ -318,11 +318,11 @@ public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher
<!-- Fix: Change exported to false --> <!-- Fix: Change exported to false -->
<receiver <receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver" android:name="org.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" /> <action android:name="org.timesafari.daily.NOTIFICATION" />
</intent-filter> </intent-filter>
</receiver> </receiver>
@@ -431,9 +431,9 @@ After implementing fixes, verify:
3. **Test receiver manually:** 3. **Test receiver manually:**
```bash ```bash
adb shell am broadcast -a com.timesafari.daily.NOTIFICATION \ adb shell am broadcast -a org.timesafari.daily.NOTIFICATION \
--es id "test_notification" \ --es id "test_notification" \
-n app.timesafari.app/com.timesafari.dailynotification.DailyNotificationReceiver -n app.timesafari.app/org.timesafari.dailynotification.DailyNotificationReceiver
``` ```
4. **Check notification permissions:** 4. **Check notification permissions:**

View File

@@ -173,10 +173,10 @@ console.log('Performance:', status.performance);
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Register new receivers --> <!-- Register new receivers -->
<receiver android:name="com.timesafari.dailynotification.NotifyReceiver" <receiver android:name="org.timesafari.dailynotification.NotifyReceiver"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<receiver android:name="com.timesafari.dailynotification.BootReceiver" <receiver android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
@@ -209,8 +209,8 @@ dependencies {
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.content-fetch</string> <string>org.timesafari.dailynotification.content-fetch</string>
<string>com.timesafari.dailynotification.notification-delivery</string> <string>org.timesafari.dailynotification.notification-delivery</string>
</array> </array>
``` ```

View File

@@ -88,7 +88,7 @@ This guide provides solutions to common iOS-specific issues when using the Daily
1. **Check BGTaskScheduler Registration:** 1. **Check BGTaskScheduler Registration:**
```swift ```swift
// Verify registration in AppDelegate // Verify registration in AppDelegate
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.timesafari.dailynotification.fetch", using: nil) { task in BGTaskScheduler.shared.register(forTaskWithIdentifier: "org.timesafari.dailynotification.fetch", using: nil) { task in
// Handler should be registered // Handler should be registered
} }
``` ```
@@ -97,7 +97,7 @@ This guide provides solutions to common iOS-specific issues when using the Daily
```xml ```xml
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.fetch</string> <string>org.timesafari.dailynotification.fetch</string>
</array> </array>
``` ```
@@ -364,7 +364,7 @@ print("Registered tasks: \(registered)")
**LLDB Command in Xcode:** **LLDB Command in Xcode:**
```lldb ```lldb
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"org.timesafari.dailynotification.fetch"]
``` ```
**Note:** This only works in simulator, not on physical devices. **Note:** This only works in simulator, not on physical devices.

View File

@@ -75,10 +75,10 @@
**How to Run:** **How to Run:**
```bash ```bash
# Run all combined edge case tests # Run all combined edge case tests
cd android && ./gradlew test --tests "com.timesafari.dailynotification.DailyNotificationRecoveryTests" cd android && ./gradlew test --tests "org.timesafari.dailynotification.DailyNotificationRecoveryTests"
# Or run specific test # Or run specific test
cd android && ./gradlew test --tests "com.timesafari.dailynotification.DailyNotificationRecoveryTests.test_combined_dst_boundary_duplicate_delivery_cold_start" cd android && ./gradlew test --tests "org.timesafari.dailynotification.DailyNotificationRecoveryTests.test_combined_dst_boundary_duplicate_delivery_cold_start"
``` ```
--- ---

View File

@@ -97,7 +97,7 @@ Before starting, verify:
**Example placeholder test:** **Example placeholder test:**
```kotlin ```kotlin
package com.timesafari.dailynotification package org.timesafari.dailynotification
import org.junit.Test import org.junit.Test
import org.junit.Assert.* import org.junit.Assert.*
@@ -148,7 +148,7 @@ class DailyNotificationRecoveryTests {
**Example structure:** **Example structure:**
```kotlin ```kotlin
package com.timesafari.dailynotification package org.timesafari.dailynotification
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase

View File

@@ -793,7 +793,7 @@ Add to `Info.plist`:
\`\`\`xml \`\`\`xml
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.fetch</string> <string>org.timesafari.dailynotification.fetch</string>
</array> </array>
\`\`\` \`\`\`

View File

@@ -13,7 +13,7 @@ This guide provides comprehensive testing procedures for the **fixed BootReceive
### **1. AndroidManifest.xml Updates** ### **1. AndroidManifest.xml Updates**
```xml ```xml
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="org.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true" android:exported="true"
android:directBootAware="true"> android:directBootAware="true">
@@ -80,13 +80,13 @@ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
**Steps**: **Steps**:
```bash ```bash
# Check BootReceiver registration # Check BootReceiver registration
adb shell "dumpsys package com.timesafari.dailynotification | grep -A10 -B5 BootReceiver" adb shell "dumpsys package org.timesafari.dailynotification | grep -A10 -B5 BootReceiver"
``` ```
**Expected Output**: **Expected Output**:
``` ```
android.intent.action.LOCKED_BOOT_COMPLETED: android.intent.action.LOCKED_BOOT_COMPLETED:
a440fcf com.timesafari.dailynotification/.BootReceiver filter 4e5fd5c a440fcf org.timesafari.dailynotification/.BootReceiver filter 4e5fd5c
Action: "android.intent.action.LOCKED_BOOT_COMPLETED" Action: "android.intent.action.LOCKED_BOOT_COMPLETED"
Action: "android.intent.action.BOOT_COMPLETED" Action: "android.intent.action.BOOT_COMPLETED"
Action: "android.intent.action.MY_PACKAGE_REPLACED" Action: "android.intent.action.MY_PACKAGE_REPLACED"
@@ -106,7 +106,7 @@ android.intent.action.LOCKED_BOOT_COMPLETED:
**Steps**: **Steps**:
```bash ```bash
# 1. Schedule notification # 1. Schedule notification
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Tap "Test Notification" (5 minutes from now) # Tap "Test Notification" (5 minutes from now)
# 2. Verify initial scheduling # 2. Verify initial scheduling
@@ -147,7 +147,7 @@ BootReceiver: Notification recovery completed: X/X recovered
**Steps**: **Steps**:
```bash ```bash
# 1. Schedule notification # 1. Schedule notification
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Tap "Test Notification" (5 minutes from now) # Tap "Test Notification" (5 minutes from now)
# 2. Verify initial scheduling # 2. Verify initial scheduling
@@ -182,7 +182,7 @@ BootReceiver: Notification recovery completed: X/X recovered
**Steps**: **Steps**:
```bash ```bash
# 1. Schedule notification # 1. Schedule notification
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Tap "Test Notification" (5 minutes from now) # Tap "Test Notification" (5 minutes from now)
# 2. Reboot device # 2. Reboot device
@@ -211,7 +211,7 @@ BootReceiver: Locked boot completed - ready for full recovery on unlock
**Steps**: **Steps**:
```bash ```bash
# 1. Launch app # 1. Launch app
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# 2. Tap "Exact Alarm Settings" button # 2. Tap "Exact Alarm Settings" button
# Should open exact alarm settings if needed # Should open exact alarm settings if needed
@@ -230,14 +230,14 @@ adb shell "dumpsys alarm | grep SCHEDULE_EXACT_ALARM"
### **Check BootReceiver Status** ### **Check BootReceiver Status**
```bash ```bash
# Verify registration # Verify registration
adb shell "dumpsys package com.timesafari.dailynotification | grep -A10 -B5 BootReceiver" adb shell "dumpsys package org.timesafari.dailynotification | grep -A10 -B5 BootReceiver"
# Check if enabled # Check if enabled
adb shell "pm list packages -d | grep timesafari" adb shell "pm list packages -d | grep timesafari"
# Should return nothing (app not disabled) # Should return nothing (app not disabled)
# Check permissions # Check permissions
adb shell "dumpsys package com.timesafari.dailynotification | grep -A5 -B5 permission" adb shell "dumpsys package org.timesafari.dailynotification | grep -A5 -B5 permission"
``` ```
### **Monitor Boot Events** ### **Monitor Boot Events**
@@ -273,13 +273,13 @@ adb shell "dumpsys alarm | grep -A5 -B5 timesafari"
**Solutions**: **Solutions**:
```bash ```bash
# Check if receiver is registered # Check if receiver is registered
adb shell "dumpsys package com.timesafari.dailynotification | grep BootReceiver" adb shell "dumpsys package org.timesafari.dailynotification | grep BootReceiver"
# Check if app is disabled # Check if app is disabled
adb shell "pm list packages -d | grep timesafari" adb shell "pm list packages -d | grep timesafari"
# Check if permissions are granted # Check if permissions are granted
adb shell "dumpsys package com.timesafari.dailynotification | grep RECEIVE_BOOT_COMPLETED" adb shell "dumpsys package org.timesafari.dailynotification | grep RECEIVE_BOOT_COMPLETED"
``` ```
### **Issue 2: Direct Boot Errors** ### **Issue 2: Direct Boot Errors**
@@ -288,10 +288,10 @@ adb shell "dumpsys package com.timesafari.dailynotification | grep RECEIVE_BOOT_
**Solutions**: **Solutions**:
```bash ```bash
# Check Direct Boot compatibility # Check Direct Boot compatibility
adb shell "dumpsys package com.timesafari.dailynotification | grep directBootAware" adb shell "dumpsys package org.timesafari.dailynotification | grep directBootAware"
# Check device protected storage # Check device protected storage
adb shell "ls -la /data/user_de/0/com.timesafari.dailynotification/" adb shell "ls -la /data/user_de/0/org.timesafari.dailynotification/"
``` ```
### **Issue 3: Exact Alarm Permission Denied** ### **Issue 3: Exact Alarm Permission Denied**
@@ -303,7 +303,7 @@ adb shell "ls -la /data/user_de/0/com.timesafari.dailynotification/"
adb shell "dumpsys alarm | grep SCHEDULE_EXACT_ALARM" adb shell "dumpsys alarm | grep SCHEDULE_EXACT_ALARM"
# Open exact alarm settings # Open exact alarm settings
adb shell am start -a android.settings.REQUEST_SCHEDULE_EXACT_ALARM --es android.provider.extra.APP_PACKAGE com.timesafari.dailynotification adb shell am start -a android.settings.REQUEST_SCHEDULE_EXACT_ALARM --es android.provider.extra.APP_PACKAGE org.timesafari.dailynotification
``` ```
## 📊 **Success Metrics** ## 📊 **Success Metrics**

View File

@@ -31,7 +31,7 @@ This document provides comprehensive testing procedures for the DailyNotificatio
**Steps**: **Steps**:
```bash ```bash
# 1. Launch app and check channel status # 1. Launch app and check channel status
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# 2. In app UI, tap "Check Channel Status" # 2. In app UI, tap "Check Channel Status"
# 3. Verify channel exists and is enabled # 3. Verify channel exists and is enabled
@@ -59,7 +59,7 @@ adb shell "dumpsys notification | grep -A5 'daily_default'"
**Steps**: **Steps**:
```bash ```bash
# 1. Block the notification channel manually # 1. Block the notification channel manually
adb shell "am start -a android.settings.CHANNEL_NOTIFICATION_SETTINGS -e android.provider.extra.APP_PACKAGE com.timesafari.dailynotification -e android.provider.extra.CHANNEL_ID daily_default" adb shell "am start -a android.settings.CHANNEL_NOTIFICATION_SETTINGS -e android.provider.extra.APP_PACKAGE org.timesafari.dailynotification -e android.provider.extra.CHANNEL_ID daily_default"
# 2. In system settings, disable the channel # 2. In system settings, disable the channel
# 3. Return to app and tap "Check Channel Status" # 3. Return to app and tap "Check Channel Status"
@@ -304,7 +304,7 @@ adb logcat -d | grep -i "recovery.*count"
**Steps**: **Steps**:
```bash ```bash
# 1. Schedule notification # 1. Schedule notification
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Tap "Test Notification" in UI # Tap "Test Notification" in UI
# 2. Verify initial scheduling # 2. Verify initial scheduling
@@ -383,7 +383,7 @@ adb logcat -d | grep -i "recovery.*performed.*recently.*skipping"
set -e set -e
APP_PACKAGE="com.timesafari.dailynotification" APP_PACKAGE="org.timesafari.dailynotification"
APP_ACTIVITY=".MainActivity" APP_ACTIVITY=".MainActivity"
TEST_TIMEOUT=300 # 5 minutes TEST_TIMEOUT=300 # 5 minutes
@@ -645,7 +645,7 @@ import json
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
class DailyNotificationTesterV2: class DailyNotificationTesterV2:
def __init__(self, package: str = "com.timesafari.dailynotification"): def __init__(self, package: str = "org.timesafari.dailynotification"):
self.package = package self.package = package
self.activity = f"{package}/.MainActivity" self.activity = f"{package}/.MainActivity"
self.test_results: Dict[str, bool] = {} self.test_results: Dict[str, bool] = {}
@@ -905,7 +905,7 @@ adb shell "svc data disable"
**Steps**: **Steps**:
```bash ```bash
# 1. Enable battery optimization for app # 1. Enable battery optimization for app
adb shell "dumpsys deviceidle whitelist -com.timesafari.dailynotification" adb shell "dumpsys deviceidle whitelist -org.timesafari.dailynotification"
# 2. Schedule notification # 2. Schedule notification
# 3. Wait for notification # 3. Wait for notification
@@ -945,7 +945,7 @@ adb shell "dumpsys deviceidle whitelist -com.timesafari.dailynotification"
**Steps**: **Steps**:
```bash ```bash
# 1. Check initial memory usage # 1. Check initial memory usage
adb shell "dumpsys meminfo com.timesafari.dailynotification" adb shell "dumpsys meminfo org.timesafari.dailynotification"
# 2. Schedule multiple notifications # 2. Schedule multiple notifications
# 3. Check memory usage again # 3. Check memory usage again
@@ -1005,7 +1005,7 @@ adb logcat -d | grep -i "channelmanager"
adb shell "dumpsys alarm | grep SCHEDULE_EXACT_ALARM" adb shell "dumpsys alarm | grep SCHEDULE_EXACT_ALARM"
# Open exact alarm settings # Open exact alarm settings
adb shell "am start -a android.settings.REQUEST_SCHEDULE_EXACT_ALARM -d package:com.timesafari.dailynotification" adb shell "am start -a android.settings.REQUEST_SCHEDULE_EXACT_ALARM -d package:org.timesafari.dailynotification"
``` ```
#### Issue 3: JIT Refresh Not Working #### Issue 3: JIT Refresh Not Working
@@ -1040,10 +1040,10 @@ adb logcat -d | grep -i "recovery.*performed.*recently"
# Comprehensive status check # Comprehensive status check
adb shell "dumpsys notification | grep -A10 daily_default" adb shell "dumpsys notification | grep -A10 daily_default"
adb shell "dumpsys alarm | grep -A5 timesafari" adb shell "dumpsys alarm | grep -A5 timesafari"
adb shell "dumpsys package com.timesafari.dailynotification | grep -A5 receiver" adb shell "dumpsys package org.timesafari.dailynotification | grep -A5 receiver"
# Recovery state check # Recovery state check
adb shell "run-as com.timesafari.dailynotification ls -la /data/data/com.timesafari.dailynotification/shared_prefs/" adb shell "run-as org.timesafari.dailynotification ls -la /data/data/org.timesafari.dailynotification/shared_prefs/"
# Channel status check # Channel status check
adb shell "cmd notification list | grep daily_default" adb shell "cmd notification list | grep daily_default"

View File

@@ -188,10 +188,10 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk
```bash ```bash
# Launch the app # Launch the app
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Alternative: Launch with specific intent # Alternative: Launch with specific intent
adb shell am start -a android.intent.action.MAIN -n com.timesafari.dailynotification/.MainActivity adb shell am start -a android.intent.action.MAIN -n org.timesafari.dailynotification/.MainActivity
``` ```
### 8. Monitor App Logs ### 8. Monitor App Logs
@@ -231,7 +231,7 @@ cd android && ./gradlew :app:assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk adb install app/build/outputs/apk/debug/app-debug.apk
# 6. Launch app # 6. Launch app
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# 7. Monitor logs # 7. Monitor logs
adb logcat -s "Capacitor" "DailyNotification" "Console" adb logcat -s "Capacitor" "DailyNotification" "Console"
@@ -259,7 +259,7 @@ npx cap run android
cd android cd android
./gradlew :app:assembleDebug ./gradlew :app:assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk adb install app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
``` ```
### Method 3: Using Monkey (Alternative Launch) ### Method 3: Using Monkey (Alternative Launch)
@@ -267,7 +267,7 @@ adb shell am start -n com.timesafari.dailynotification/.MainActivity
```bash ```bash
# Install and launch with Monkey # Install and launch with Monkey
adb install app/build/outputs/apk/debug/app-debug.apk adb install app/build/outputs/apk/debug/app-debug.apk
adb shell monkey -p com.timesafari.dailynotification -c android.intent.category.LAUNCHER 1 adb shell monkey -p org.timesafari.dailynotification -c android.intent.category.LAUNCHER 1
``` ```
## Troubleshooting ## Troubleshooting
@@ -318,7 +318,7 @@ adb version
adb shell pm list packages | grep timesafari adb shell pm list packages | grep timesafari
# Uninstall existing app # Uninstall existing app
adb uninstall com.timesafari.dailynotification adb uninstall org.timesafari.dailynotification
# Install with force # Install with force
adb install -r -t app/build/outputs/apk/debug/app-debug.apk adb install -r -t app/build/outputs/apk/debug/app-debug.apk
@@ -369,7 +369,7 @@ When the app launches successfully, you should see:
```bash ```bash
# ADB output # ADB output
Starting: Intent { cmp=com.timesafari.dailynotification/.MainActivity } Starting: Intent { cmp=org.timesafari.dailynotification/.MainActivity }
# Logcat output # Logcat output
D Capacitor: Starting BridgeActivity D Capacitor: Starting BridgeActivity
@@ -415,7 +415,7 @@ The app should display:
./scripts/build-native.sh --platform android ./scripts/build-native.sh --platform android
cd android && ./gradlew :app:assembleDebug cd android && ./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
``` ```
### Automated Testing ### Automated Testing
@@ -426,7 +426,7 @@ adb wait-for-device
./scripts/build-native.sh --platform android ./scripts/build-native.sh --platform android
cd android && ./gradlew :app:assembleDebug cd android && ./gradlew :app:assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk adb install app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n com.timesafari.dailynotification/.MainActivity adb shell am start -n org.timesafari.dailynotification/.MainActivity
# Run tests... # Run tests...
``` ```

View File

@@ -54,7 +54,7 @@ po UNUserNotificationCenter.current().pendingNotificationRequests()
po await UNUserNotificationCenter.current().notificationSettings() po await UNUserNotificationCenter.current().notificationSettings()
// Manually trigger BGTask (simulator only) // Manually trigger BGTask (simulator only)
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"org.timesafari.dailynotification.fetch"]
``` ```
--- ---
@@ -78,7 +78,7 @@ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWith
3. **Filter logs:** 3. **Filter logs:**
- Click search box (top right) - Click search box (top right)
- Type: `DNP-` or `com.timesafari.dailynotification` - Type: `DNP-` or `org.timesafari.dailynotification`
- Press Enter - Press Enter
### Filter by Subsystem (Structured Logging): ### Filter by Subsystem (Structured Logging):
@@ -86,14 +86,14 @@ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWith
The plugin uses structured logging with subsystems: The plugin uses structured logging with subsystems:
``` ```
com.timesafari.dailynotification.plugin # Plugin operations org.timesafari.dailynotification.plugin # Plugin operations
com.timesafari.dailynotification.fetch # Fetch operations org.timesafari.dailynotification.fetch # Fetch operations
com.timesafari.dailynotification.scheduler # Scheduling operations org.timesafari.dailynotification.scheduler # Scheduling operations
com.timesafari.dailynotification.storage # Storage operations org.timesafari.dailynotification.storage # Storage operations
``` ```
**To filter by subsystem:** **To filter by subsystem:**
- In Console.app search: `subsystem:com.timesafari.dailynotification` - In Console.app search: `subsystem:org.timesafari.dailynotification`
--- ---
@@ -108,7 +108,7 @@ com.timesafari.dailynotification.storage # Storage operations
xcrun simctl spawn booted log stream xcrun simctl spawn booted log stream
# Stream only plugin logs (filtered) # Stream only plugin logs (filtered)
xcrun simctl spawn booted log stream --predicate 'subsystem == "com.timesafari.dailynotification"' xcrun simctl spawn booted log stream --predicate 'subsystem == "org.timesafari.dailynotification"'
# Stream with DNP- prefix filter # Stream with DNP- prefix filter
xcrun simctl spawn booted log stream | grep "DNP-" xcrun simctl spawn booted log stream | grep "DNP-"
@@ -121,7 +121,7 @@ xcrun simctl spawn booted log stream | grep "DNP-"
xcrun simctl spawn booted log stream > device.log 2>&1 xcrun simctl spawn booted log stream > device.log 2>&1
# Save filtered logs # Save filtered logs
xcrun simctl spawn booted log stream --predicate 'subsystem == "com.timesafari.dailynotification"' > plugin.log 2>&1 xcrun simctl spawn booted log stream --predicate 'subsystem == "org.timesafari.dailynotification"' > plugin.log 2>&1
# Then analyze with grep # Then analyze with grep
grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log
@@ -144,7 +144,7 @@ xcrun simctl spawn booted log show --start "2025-11-15 10:00:00" --end "2025-11-
xcrun devicectl list devices xcrun devicectl list devices
# Stream logs from physical device (requires device UDID) # Stream logs from physical device (requires device UDID)
xcrun devicectl device process launch --device <UDID> com.timesafari.dailynotification.test xcrun devicectl device process launch --device <UDID> org.timesafari.dailynotification.test
# Or use Console.app for physical devices (easier) # Or use Console.app for physical devices (easier)
``` ```
@@ -162,7 +162,7 @@ xcrun devicectl device process launch --device <UDID> com.timesafari.dailynotifi
./scripts/validate-ios-logs.sh device.log ./scripts/validate-ios-logs.sh device.log
# From live stream # From live stream
xcrun simctl spawn booted log stream --predicate 'subsystem == "com.timesafari.dailynotification"' | ./scripts/validate-ios-logs.sh xcrun simctl spawn booted log stream --predicate 'subsystem == "org.timesafari.dailynotification"' | ./scripts/validate-ios-logs.sh
# From filtered grep # From filtered grep
grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log | ./scripts/validate-ios-logs.sh grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log | ./scripts/validate-ios-logs.sh
@@ -207,7 +207,7 @@ grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log | ./scripts/validate-ios-l
**Solutions:** **Solutions:**
1. **Use specific filters:** `DNP-` instead of `DailyNotification` 1. **Use specific filters:** `DNP-` instead of `DailyNotification`
2. **Filter by subsystem:** `subsystem:com.timesafari.dailynotification` 2. **Filter by subsystem:** `subsystem:org.timesafari.dailynotification`
3. **Use time range:** Only show logs from last 5 minutes 3. **Use time range:** Only show logs from last 5 minutes
4. **Use validation script:** Automatically filters for important events 4. **Use validation script:** Automatically filters for important events
@@ -235,7 +235,7 @@ grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log | ./scripts/validate-ios-l
```bash ```bash
# Stream plugin logs (simulator) # Stream plugin logs (simulator)
xcrun simctl spawn booted log stream --predicate 'subsystem == "com.timesafari.dailynotification"' xcrun simctl spawn booted log stream --predicate 'subsystem == "org.timesafari.dailynotification"'
# Save logs to file # Save logs to file
xcrun simctl spawn booted log stream > device.log 2>&1 xcrun simctl spawn booted log stream > device.log 2>&1

View File

@@ -150,7 +150,7 @@ po UNUserNotificationCenter.current().pendingNotificationRequests()
# Check BGTask scheduling (simulator only) # Check BGTask scheduling (simulator only)
# Use LLDB command in Xcode debugger: # Use LLDB command in Xcode debugger:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"org.timesafari.dailynotification.fetch"]
``` ```
--- ---
@@ -316,7 +316,7 @@ DNP-FETCH: BGTask rescheduled for [date]
**Manual Trigger (Simulator Only):** **Manual Trigger (Simulator Only):**
```bash ```bash
# In Xcode debugger (LLDB) # In Xcode debugger (LLDB)
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"org.timesafari.dailynotification.fetch"]
``` ```
--- ---
@@ -422,8 +422,8 @@ open DailyNotificationPlugin.xcodeproj
```xml ```xml
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.timesafari.dailynotification.fetch</string> <string>org.timesafari.dailynotification.fetch</string>
<string>com.timesafari.dailynotification.notify</string> <string>org.timesafari.dailynotification.notify</string>
</array> </array>
``` ```
@@ -464,7 +464,7 @@ po await UNUserNotificationCenter.current().notificationSettings()
**Check BGTask Status (Simulator Only):** **Check BGTask Status (Simulator Only):**
```swift ```swift
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"org.timesafari.dailynotification.fetch"]
``` ```
**Check Storage:** **Check Storage:**

View File

@@ -137,7 +137,7 @@ Quick validation checklist (each step links to log verification):
- Background fetch - Background fetch
- Background processing (if using `BGProcessingTask`) - Background processing (if using `BGProcessingTask`)
- Info.plist has: - Info.plist has:
- `BGTaskSchedulerPermittedIdentifiers` array with task identifier: `com.timesafari.dailynotification.fetch` - `BGTaskSchedulerPermittedIdentifiers` array with task identifier: `org.timesafari.dailynotification.fetch`
- Plugin exposes: - Plugin exposes:
- `scheduleDailyNotification()` method that schedules both prefetch and notification - `scheduleDailyNotification()` method that schedules both prefetch and notification
@@ -186,7 +186,7 @@ Add structured logs at key points:
**On app startup:** **On app startup:**
``` ```
[DNP-FETCH] Registering BGTaskScheduler task (id=com.timesafari.dailynotification.fetch) [DNP-FETCH] Registering BGTaskScheduler task (id=org.timesafari.dailynotification.fetch)
``` ```
**When scheduling:** **When scheduling:**
@@ -197,7 +197,7 @@ Add structured logs at key points:
**When BGTask handler fires:** **When BGTask handler fires:**
``` ```
[DNP-FETCH] BGTask handler invoked (task.identifier=com.timesafari.dailynotification.fetch) [DNP-FETCH] BGTask handler invoked (task.identifier=org.timesafari.dailynotification.fetch)
``` ```
**Inside prefetch logic:** **Inside prefetch logic:**
@@ -239,7 +239,7 @@ Add structured logs at key points:
2. **Open Xcode Debug Console** (View → Debug Area → Activate Console, or press Cmd+Shift+Y) 2. **Open Xcode Debug Console** (View → Debug Area → Activate Console, or press Cmd+Shift+Y)
3. **In LLDB console, paste and execute:** 3. **In LLDB console, paste and execute:**
```bash ```bash
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"org.timesafari.dailynotification.fetch"]
``` ```
4. Press Enter 4. Press Enter
@@ -270,7 +270,7 @@ Add structured logs at key points:
**Troubleshooting:** **Troubleshooting:**
- If LLDB command doesn't work, ensure the app is backgrounded first - If LLDB command doesn't work, ensure the app is backgrounded first
- If you see "Simulate Background Fetch" in menu when app is running, it may be a different Xcode version - try the LLDB method anyway - If you see "Simulate Background Fetch" in menu when app is running, it may be a different Xcode version - try the LLDB method anyway
- Verify BGTask identifier matches exactly: `com.timesafari.dailynotification.fetch` - Verify BGTask identifier matches exactly: `org.timesafari.dailynotification.fetch`
### 5. Trigger or Wait for Notification ### 5. Trigger or Wait for Notification
@@ -544,7 +544,7 @@ When everything is wired correctly, one full cycle should produce:
**Expected logs:** **Expected logs:**
``` ```
[DNP-FETCH] Registering BGTaskScheduler task (id=com.timesafari.dailynotification.fetch) [DNP-FETCH] Registering BGTaskScheduler task (id=org.timesafari.dailynotification.fetch)
[DNP-PLUGIN] Startup complete (hasPendingSchedules=true|false) [DNP-PLUGIN] Startup complete (hasPendingSchedules=true|false)
``` ```
@@ -585,7 +585,7 @@ When everything is wired correctly, one full cycle should produce:
**Expected logs:** **Expected logs:**
``` ```
[DNP-FETCH] BGTask handler invoked (id=com.timesafari.dailynotification.fetch) [DNP-FETCH] BGTask handler invoked (id=org.timesafari.dailynotification.fetch)
[DNP-FETCH] Resolved next notification needing content (time=..., scheduleId=...) [DNP-FETCH] Resolved next notification needing content (time=..., scheduleId=...)
[DNP-FETCH] Starting fetch from <URL> (notificationTime=..., jwtPresent=true) [DNP-FETCH] Starting fetch from <URL> (notificationTime=..., jwtPresent=true)
[DNP-FETCH] Fetch success (status=200, bytes=1234, ttl=86400) [DNP-FETCH] Fetch success (status=200, bytes=1234, ttl=86400)

Some files were not shown because too many files have changed in this diff Show More