feat(android): integrate daily notification plugin with native fetcher

Add native Android components for daily notification plugin integration:
- TimeSafariApplication: Custom Application class to register native fetcher
- TimeSafariNativeFetcher: Implements NativeNotificationContentFetcher interface
- network_security_config.xml: Allow cleartext for local development

Configuration updates:
- AndroidManifest.xml: Link custom Application class, add required permissions
- build.gradle: Add Java 17 compile options and required dependencies
- capacitor.config.ts: Add DailyNotification plugin configuration
- NativeNotificationService.ts: Use "daily_" prefixed ID for schedule rollover

Note: Subsequent notification scheduling after first fire still has issues
that require further investigation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jose Olarte III
2026-02-12 21:20:52 +08:00
parent a7fbb26847
commit c05dff6654
9 changed files with 188 additions and 12 deletions

View File

@@ -27,6 +27,12 @@ if (!project.ext.MY_KEYSTORE_FILE) {
android {
namespace 'app.timesafari'
compileSdk rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
defaultConfig {
applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion
@@ -101,13 +107,20 @@ dependencies {
implementation project(':capacitor-android')
implementation project(':capacitor-community-sqlite')
implementation "androidx.biometric:biometric:1.2.0-alpha05"
// Daily Notification Plugin dependencies
implementation "androidx.room:room-runtime:2.6.1"
implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
annotationProcessor "androidx.room:room-compiler:2.6.1"
// Capacitor annotation processor for automatic plugin discovery
annotationProcessor project(':capacitor-android')
// Additional dependencies for notification plugin
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
implementation 'com.google.code.gson:gson:2.10.1'
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"

View File

@@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".TimeSafariApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
@@ -49,7 +51,7 @@
<receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true"
android:exported="true">
android:exported="false">
<intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" />
</intent-filter>
@@ -99,4 +101,8 @@
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
</manifest>

View File

@@ -42,6 +42,31 @@
"biometricTitle": "Biometric login for TimeSafari"
},
"electronIsEncryption": false
},
"DailyNotification": {
"debugMode": true,
"enableNotifications": true,
"timesafariConfig": {
"activeDid": "",
"endpoints": {
"projectsLastUpdated": "https://api.endorser.ch/api/v2/report/plansLastUpdatedBetween"
},
"starredProjectsConfig": {
"enabled": true,
"starredPlanHandleIds": [],
"fetchInterval": "0 8 * * *"
}
},
"networkConfig": {
"timeout": 30000,
"retryAttempts": 3,
"retryDelay": 1000
},
"contentFetch": {
"enabled": true,
"schedule": "0 8 * * *",
"fetchLeadTimeMinutes": 5
}
}
},
"ios": {

View File

@@ -38,13 +38,5 @@
{
"pkg": "@timesafari/daily-notification-plugin",
"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"
},
{
"pkg": "SafeArea",
"classpath": "app.timesafari.safearea.SafeAreaPlugin"
},
{
"pkg": "SharedImage",
"classpath": "app.timesafari.sharedimage.SharedImagePlugin"
}
]

View File

@@ -0,0 +1,27 @@
package app.timesafari;
import android.app.Application;
import android.content.Context;
import android.util.Log;
import com.timesafari.dailynotification.DailyNotificationPlugin;
import com.timesafari.dailynotification.NativeNotificationContentFetcher;
public class TimeSafariApplication extends Application {
private static final String TAG = "TimeSafariApplication";
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "Initializing TimeSafari notifications");
// Register native fetcher with application context
Context context = getApplicationContext();
NativeNotificationContentFetcher fetcher =
new TimeSafariNativeFetcher(context);
DailyNotificationPlugin.setNativeFetcher(fetcher);
Log.i(TAG, "Native fetcher registered");
}
}

View File

@@ -0,0 +1,73 @@
package app.timesafari;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import com.timesafari.dailynotification.FetchContext;
import com.timesafari.dailynotification.NativeNotificationContentFetcher;
import com.timesafari.dailynotification.NotificationContent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class TimeSafariNativeFetcher implements NativeNotificationContentFetcher {
private static final String TAG = "TimeSafariNativeFetcher";
private final Context context;
// Configuration from TypeScript (set via configure())
private volatile String apiBaseUrl;
private volatile String activeDid;
private volatile String jwtToken;
public TimeSafariNativeFetcher(Context context) {
this.context = context;
}
@Override
public void configure(String apiBaseUrl, String activeDid, String jwtToken) {
this.apiBaseUrl = apiBaseUrl;
this.activeDid = activeDid;
this.jwtToken = jwtToken;
Log.i(TAG, "Fetcher configured with API: " + apiBaseUrl + ", DID: " + activeDid);
}
@NonNull
@Override
public CompletableFuture<List<NotificationContent>> fetchContent(@NonNull FetchContext fetchContext) {
Log.d(TAG, "Fetching notification content, trigger: " + fetchContext.trigger);
return CompletableFuture.supplyAsync(() -> {
try {
// TODO: Implement actual content fetching for TimeSafari
// This should query the TimeSafari API for notification content
// using the configured apiBaseUrl, activeDid, and jwtToken
// For now, return a placeholder notification
long scheduledTime = fetchContext.scheduledTime != null
? fetchContext.scheduledTime
: System.currentTimeMillis() + 60000; // 1 minute from now
NotificationContent content = new NotificationContent(
"TimeSafari Update",
"Check your starred projects for updates!",
scheduledTime
);
List<NotificationContent> results = new ArrayList<>();
results.add(content);
Log.d(TAG, "Returning " + results.size() + " notification(s)");
return results;
} catch (Exception e) {
Log.e(TAG, "Fetch failed", e);
return Collections.emptyList();
}
});
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
</network-security-config>

View File

@@ -44,6 +44,31 @@ const config: CapacitorConfig = {
biometricTitle: 'Biometric login for TimeSafari'
},
electronIsEncryption: false
},
DailyNotification: {
debugMode: true,
enableNotifications: true,
timesafariConfig: {
activeDid: '', // Will be set dynamically from user's DID
endpoints: {
projectsLastUpdated: 'https://api.endorser.ch/api/v2/report/plansLastUpdatedBetween'
},
starredProjectsConfig: {
enabled: true,
starredPlanHandleIds: [],
fetchInterval: '0 8 * * *'
}
},
networkConfig: {
timeout: 30000,
retryAttempts: 3,
retryDelay: 1000
},
contentFetch: {
enabled: true,
schedule: '0 8 * * *',
fetchLeadTimeMinutes: 5
}
}
},
ios: {

View File

@@ -40,7 +40,10 @@ import { logger } from "@/utils/logger";
* - Native OS notification UI
*/
export class NativeNotificationService implements NotificationServiceInterface {
private readonly reminderId = "timesafari_daily_reminder";
// IMPORTANT: ID must start with "daily_" for proper schedule rollover handling
// The plugin's scheduleNextNotification() preserves IDs starting with "daily_"
// but replaces others with random "daily_rollover_xxx" IDs, causing conflicts
private readonly reminderId = "daily_timesafari_reminder";
private readonly platformName = "native";
/**