refactor(android)!: restructure to standard Capacitor plugin layout

Restructure Android project from nested module layout to standard
Capacitor plugin structure following community conventions.

Structure Changes:
- Move plugin code from android/plugin/ to android/src/main/java/
- Move test app from android/app/ to test-apps/android-test-app/app/
- Remove nested android/plugin module structure
- Remove nested android/app test app structure

Build Infrastructure:
- Add Gradle wrapper files (gradlew, gradlew.bat, gradle/wrapper/)
- Transform android/build.gradle from root project to library module
- Update android/settings.gradle for standalone plugin builds
- Add android/gradle.properties with AndroidX configuration
- Add android/consumer-rules.pro for ProGuard rules

Configuration Updates:
- Add prepare script to package.json for automatic builds on npm install
- Update package.json version to 1.0.1
- Update android/build.gradle to properly resolve Capacitor dependencies
- Update test-apps/android-test-app/settings.gradle with correct paths
- Remove android/variables.gradle (hardcode values in build.gradle)

Documentation:
- Update BUILDING.md with new structure and build process
- Update INTEGRATION_GUIDE.md to reflect standard structure
- Update README.md to remove path fix warnings
- Add test-apps/BUILD_PROCESS.md documenting test app build flows

Test App Configuration:
- Fix android-test-app to correctly reference plugin and Capacitor
- Remove capacitor-cordova-android-plugins dependency (not needed)
- Update capacitor.settings.gradle path verification in fix script

BREAKING CHANGE: Plugin now uses standard Capacitor Android structure.
Consuming apps must update their capacitor.settings.gradle to reference
android/ instead of android/plugin/. This is automatically handled by
Capacitor CLI for apps using standard plugin installation.
This commit is contained in:
Matthew Raymer
2025-11-05 08:08:37 +00:00
parent c4b7f6382f
commit d9bdeb6d02
128 changed files with 1654 additions and 1747 deletions

235
test-apps/BUILD_PROCESS.md Normal file
View File

@@ -0,0 +1,235 @@
# Test Apps Build Process Review
## Summary
Both test apps are configured to **automatically build the plugin** as part of their build process. The plugin is included as a Gradle project dependency, so Gradle handles building it automatically.
---
## Test App 1: `android-test-app` (Standalone Android)
**Location**: `test-apps/android-test-app/`
### Configuration
**Plugin Reference** (`settings.gradle`):
```gradle
// Reference plugin from root project
def pluginPath = new File(settingsDir.parentFile.parentFile, 'android')
include ':daily-notification-plugin'
project(':daily-notification-plugin').projectDir = pluginPath
```
**Plugin Dependency** (`app/build.gradle`):
```gradle
dependencies {
implementation project(':capacitor-android')
implementation project(':daily-notification-plugin') // ✅ Plugin included
// Plugin dependencies also included
}
```
**Capacitor Setup**:
- References Capacitor from `daily-notification-test/node_modules/` (shared dependency)
- Includes `:capacitor-android` project module
### Build Process
1. **Gradle resolves plugin project** - Finds plugin at `../../android`
2. **Gradle builds plugin module** - Compiles plugin Java code to AAR (internally)
3. **Gradle builds app module** - Compiles app code
4. **Gradle links plugin** - Includes plugin classes in app APK
5. **Final output**: `app/build/outputs/apk/debug/app-debug.apk`
### Build Commands
```bash
cd test-apps/android-test-app
# Build debug APK (builds plugin automatically)
./gradlew assembleDebug
# Build release APK
./gradlew assembleRelease
# Clean build
./gradlew clean
# List tasks
./gradlew tasks
```
### Prerequisites
- ✅ Gradle wrapper present (`gradlew`, `gradlew.bat`, `gradle/wrapper/`)
- ✅ Capacitor must be installed in `daily-notification-test/node_modules/` (shared)
- ✅ Plugin must exist at root `android/` directory
---
## Test App 2: `daily-notification-test` (Vue 3 + Capacitor)
**Location**: `test-apps/daily-notification-test/`
### Configuration
**Plugin Installation** (`package.json`):
```json
{
"dependencies": {
"@timesafari/daily-notification-plugin": "file:../../"
}
}
```
**Capacitor Auto-Configuration**:
- `npx cap sync android` automatically:
1. Installs plugin from `file:../../``node_modules/@timesafari/daily-notification-plugin/`
2. Generates `capacitor.settings.gradle` with plugin reference
3. Generates `capacitor.build.gradle` with plugin dependency
4. Generates `capacitor.plugins.json` with plugin registration
**Plugin Reference** (`capacitor.settings.gradle` - auto-generated):
```gradle
include ':timesafari-daily-notification-plugin'
project(':timesafari-daily-notification-plugin').projectDir =
new File('../node_modules/@timesafari/daily-notification-plugin/android')
```
**Plugin Dependency** (`capacitor.build.gradle` - auto-generated):
```gradle
dependencies {
implementation project(':timesafari-daily-notification-plugin')
}
```
### Build Process
1. **npm install** - Installs plugin from `file:../../` to `node_modules/`
2. **npm run build** - Builds Vue 3 web app → `dist/`
3. **npx cap sync android** - Capacitor:
- Copies web assets to `android/app/src/main/assets/`
- Configures plugin in Gradle files
- Registers plugin in `capacitor.plugins.json`
4. **Fix script runs** - Verifies plugin path is correct (post-sync hook)
5. **Gradle builds** - Plugin is built as part of app build
6. **Final output**: `android/app/build/outputs/apk/debug/app-debug.apk`
### Build Commands
```bash
cd test-apps/daily-notification-test
# Initial setup (one-time)
npm install # Installs plugin from file:../../
npx cap sync android # Configures Android build
# Development workflow
npm run build # Builds Vue 3 web app
npx cap sync android # Syncs web assets + plugin config
cd android
./gradlew assembleDebug # Builds Android app (includes plugin)
# Or use Capacitor CLI (does everything)
npx cap run android # Builds web + syncs + builds Android + runs
```
### Post-Install Hook
The `postinstall` script (`scripts/fix-capacitor-plugins.js`) automatically:
- ✅ Verifies plugin is registered in `capacitor.plugins.json`
- ✅ Verifies plugin path in `capacitor.settings.gradle` points to `android/` (standard structure)
- ✅ Fixes path if it incorrectly points to old `android/plugin/` structure
---
## Key Points
### ✅ Both Apps Build Plugin Automatically
- **No manual plugin build needed** - Gradle handles it
- **Plugin is a project dependency** - Built before the app
- **Standard Gradle behavior** - Works like any Android library module
### ✅ Plugin Structure is Standard
- **Plugin location**: `android/src/main/java/...` (standard Capacitor structure)
- **No path fixes needed** - Capacitor auto-generates correct paths
- **Works with `npx cap sync`** - No manual configuration required
### ✅ Build Dependencies
**android-test-app**:
- Requires Capacitor from `daily-notification-test/node_modules/` (shared)
- References plugin directly from root `android/` directory
**daily-notification-test**:
- Requires `npm install` to install plugin
- Requires `npx cap sync android` to configure build
- Plugin installed to `node_modules/` like any npm package
---
## Verification
### Check Plugin is Included
```bash
# For android-test-app
cd test-apps/android-test-app
./gradlew :app:dependencies | grep daily-notification
# For daily-notification-test
cd test-apps/daily-notification-test/android
./gradlew :app:dependencies | grep timesafari
```
### Check Plugin Registration
```bash
# Vue app only
cat test-apps/daily-notification-test/android/app/src/main/assets/capacitor.plugins.json
```
Should contain:
```json
[
{
"name": "DailyNotification",
"classpath": "com.timesafari.dailynotification.DailyNotificationPlugin"
}
]
```
---
## Troubleshooting
### android-test-app: "Capacitor not found"
**Solution**: Run `npm install` in `test-apps/daily-notification-test/` first to install Capacitor dependencies.
### android-test-app: "Plugin not found"
**Solution**: Verify `android/build.gradle` exists at the root project level.
### daily-notification-test: Plugin path wrong
**Solution**: Run `node scripts/fix-capacitor-plugins.js` after `npx cap sync android`. The script now verifies/fixes the path to use standard `android/` structure.
### Both: Build succeeds but plugin doesn't work
**Solution**:
- Check `capacitor.plugins.json` has plugin registered
- Verify plugin classes are in the APK: `unzip -l app-debug.apk | grep DailyNotification`
---
## Summary
**Both test apps handle plugin building automatically**
**Plugin uses standard Capacitor structure** (`android/src/main/java/`)
**No manual plugin builds required** - Gradle handles dependencies
**Build processes are configured correctly** - Ready to use
The test apps are properly configured to build and test the plugin!

View File

@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep

View File

@@ -0,0 +1,62 @@
apply plugin: 'com.android.application'
android {
namespace "com.timesafari.dailynotification"
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.timesafari.dailynotification"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android')
implementation project(':daily-notification-plugin')
// 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"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
// Note: capacitor-cordova-android-plugins not needed for standalone Android test app
}
apply from: 'capacitor.build.gradle'
try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}

View File

@@ -0,0 +1,20 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
// Plugin development project - no Capacitor integration files needed
// apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}

View File

@@ -0,0 +1,154 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# =============================================================================
# Capacitor Plugin Protection Rules
# =============================================================================
# Keep Capacitor Plugin annotations & your plugin facade
-keep class com.getcapacitor.** { *; }
-keep @com.getcapacitor.annotation.CapacitorPlugin class * { *; }
-keepclassmembers class ** {
@com.getcapacitor.annotation.PluginMethod *;
}
# Keep DailyNotification plugin classes
-keep class com.timesafari.dailynotification.** { *; }
# Keep plugin method names and signatures
-keepclassmembers class com.timesafari.dailynotification.DailyNotificationPlugin {
public *;
}
# Keep all plugin manager classes
-keep class com.timesafari.dailynotification.*Manager { *; }
-keep class com.timesafari.dailynotification.*Storage { *; }
-keep class com.timesafari.dailynotification.*Receiver { *; }
# Keep Room database classes
-keep class com.timesafari.dailynotification.storage.** { *; }
-keep class com.timesafari.dailynotification.database.** { *; }
# Keep error handling classes
-keep class com.timesafari.dailynotification.*Error* { *; }
-keep class com.timesafari.dailynotification.*Exception* { *; }
# Keep JWT and ETag managers
-keep class com.timesafari.dailynotification.*JWT* { *; }
-keep class com.timesafari.dailynotification.*ETag* { *; }
# Keep performance and optimization classes
-keep class com.timesafari.dailynotification.*Performance* { *; }
-keep class com.timesafari.dailynotification.*Optimizer* { *; }
# Keep rolling window and TTL classes
-keep class com.timesafari.dailynotification.*Rolling* { *; }
-keep class com.timesafari.dailynotification.*TTL* { *; }
# Keep exact alarm and reboot recovery classes
-keep class com.timesafari.dailynotification.*Exact* { *; }
-keep class com.timesafari.dailynotification.*Reboot* { *; }
-keep class com.timesafari.dailynotification.*Recovery* { *; }
# Keep enhanced fetcher classes
-keep class com.timesafari.dailynotification.*Enhanced* { *; }
-keep class com.timesafari.dailynotification.*Fetcher* { *; }
# Keep migration classes
-keep class com.timesafari.dailynotification.*Migration* { *; }
# Keep channel manager
-keep class com.timesafari.dailynotification.ChannelManager { *; }
# Keep permission manager
-keep class com.timesafari.dailynotification.PermissionManager { *; }
# Keep scheduler classes
-keep class com.timesafari.dailynotification.*Scheduler* { *; }
# =============================================================================
# Android System Classes
# =============================================================================
# Keep Android system classes used by the plugin
-keep class android.app.AlarmManager { *; }
-keep class android.app.NotificationManager { *; }
-keep class android.app.NotificationChannel { *; }
-keep class android.app.PendingIntent { *; }
-keep class androidx.work.WorkManager { *; }
-keep class androidx.core.app.NotificationCompat { *; }
# Keep broadcast receiver classes
-keep class android.content.BroadcastReceiver { *; }
-keep class android.content.Intent { *; }
# =============================================================================
# Room Database Protection
# =============================================================================
# Keep Room database classes
-keep class androidx.room.** { *; }
-keep class * extends androidx.room.RoomDatabase { *; }
-keep @androidx.room.Entity class * { *; }
-keep @androidx.room.Dao class * { *; }
# Keep Room database migrations
-keep class * extends androidx.room.migration.Migration { *; }
# =============================================================================
# Gson Protection (if used)
# =============================================================================
# Keep Gson classes if used for JSON serialization
-keep class com.google.gson.** { *; }
-keep class * implements com.google.gson.TypeAdapterFactory { *; }
-keep class * implements com.google.gson.JsonSerializer { *; }
-keep class * implements com.google.gson.JsonDeserializer { *; }
# =============================================================================
# Debugging and Development
# =============================================================================
# Keep debug information for development builds
-keepattributes SourceFile,LineNumberTable
-keepattributes Signature
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes EnclosingMethod
# Keep generic signatures for reflection
-keepattributes Signature
# Keep annotations
-keepattributes *Annotation*
# =============================================================================
# Network and Security
# =============================================================================
# Keep network-related classes
-keep class java.net.** { *; }
-keep class javax.net.ssl.** { *; }
# Keep security-related classes
-keep class java.security.** { *; }
-keep class javax.crypto.** { *; }

View File

@@ -0,0 +1,26 @@
package com.timesafari.dailynotification;
import static org.junit.Assert.*;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.timesafari.dailynotification", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".PluginApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Daily Notification Plugin Receivers -->
<receiver
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" />
</intent-filter>
</receiver>
<receiver
android:name="com.timesafari.dailynotification.BootReceiver"
android:enabled="true"
android:exported="true"
android:directBootAware="true">
<intent-filter android:priority="1000">
<!-- Delivered very early after reboot (before unlock) -->
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<!-- Delivered after the user unlocks / credential-encrypted storage is available -->
<action android:name="android.intent.action.BOOT_COMPLETED" />
<!-- Delivered after app update; great for rescheduling alarms without reboot -->
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
</manifest>

View File

@@ -0,0 +1,16 @@
{
"appId": "com.timesafari.dailynotification",
"appName": "DailyNotification Test App",
"webDir": "www",
"server": {
"androidScheme": "https"
},
"plugins": {
"DailyNotification": {
"fetchUrl": "https://api.example.com/daily-content",
"scheduleTime": "09:00",
"enableNotifications": true,
"debugMode": true
}
}
}

View File

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

View File

@@ -0,0 +1,575 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>DailyNotification Plugin Test</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: white;
}
.container {
max-width: 600px;
margin: 0 auto;
text-align: center;
}
h1 {
margin-bottom: 30px;
font-size: 2.5em;
}
.button {
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 15px 30px;
margin: 10px;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.button:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.status {
margin-top: 30px;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
font-family: monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>🔔 DailyNotification Plugin Test</h1>
<p>Test the DailyNotification plugin functionality</p>
<p style="font-size: 12px; opacity: 0.8;">Build: 2025-10-14 05:00:00 UTC</p>
<button class="button" onclick="testPlugin()">Test Plugin</button>
<button class="button" onclick="configurePlugin()">Configure Plugin</button>
<button class="button" onclick="checkStatus()">Check Status</button>
<h2>🔔 Notification Tests</h2>
<button class="button" onclick="testNotification()">Test Notification</button>
<button class="button" onclick="scheduleNotification()">Schedule Notification</button>
<button class="button" onclick="showReminder()">Show Reminder</button>
<h2>🔐 Permission Management</h2>
<button class="button" onclick="checkPermissions()">Check Permissions</button>
<button class="button" onclick="requestPermissions()">Request Permissions</button>
<button class="button" onclick="openExactAlarmSettings()">Exact Alarm Settings</button>
<h2>📢 Channel Management</h2>
<button class="button" onclick="checkChannelStatus()">Check Channel Status</button>
<button class="button" onclick="openChannelSettings()">Open Channel Settings</button>
<button class="button" onclick="checkComprehensiveStatus()">Comprehensive Status</button>
<div id="status" class="status">
Ready to test...
</div>
</div>
<script>
console.log('Script loading...');
console.log('JavaScript is working!');
// Use real DailyNotification plugin
console.log('Using real DailyNotification plugin...');
window.DailyNotification = window.Capacitor.Plugins.DailyNotification;
// Define functions immediately and attach to window
function testPlugin() {
console.log('testPlugin called');
const status = document.getElementById('status');
status.innerHTML = 'Testing plugin...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
// Plugin is loaded and ready
status.innerHTML = 'Plugin is loaded and ready!';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
} catch (error) {
status.innerHTML = `Plugin test failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
function configurePlugin() {
console.log('configurePlugin called');
const status = document.getElementById('status');
status.innerHTML = 'Configuring plugin...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
// Configure plugin settings
window.DailyNotification.configure({
storage: 'tiered',
ttlSeconds: 86400,
prefetchLeadMinutes: 60,
maxNotificationsPerDay: 3,
retentionDays: 7
})
.then(() => {
console.log('Plugin settings configured, now configuring native fetcher...');
// Configure native fetcher with demo credentials
// Note: DemoNativeFetcher uses hardcoded mock data, so this is optional
// but demonstrates the API. In production, this would be real credentials.
return window.DailyNotification.configureNativeFetcher({
apiBaseUrl: 'http://10.0.2.2:3000', // Android emulator → host localhost
activeDid: 'did:ethr:0xDEMO1234567890', // Demo DID
jwtSecret: 'demo-jwt-secret-for-development-testing'
});
})
.then(() => {
status.innerHTML = 'Plugin configured successfully!<br>✅ Plugin settings<br>✅ Native fetcher (optional for demo)';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
})
.catch(error => {
status.innerHTML = `Configuration failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Configuration failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
function checkStatus() {
console.log('checkStatus called');
const status = document.getElementById('status');
status.innerHTML = 'Checking plugin status...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
window.DailyNotification.getNotificationStatus()
.then(result => {
const nextTime = result.nextNotificationTime ? new Date(result.nextNotificationTime).toLocaleString() : 'None scheduled';
status.innerHTML = `Plugin Status:<br>
Enabled: ${result.isEnabled}<br>
Next Notification: ${nextTime}<br>
Pending: ${result.pending}<br>
Settings: ${JSON.stringify(result.settings)}`;
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
})
.catch(error => {
status.innerHTML = `Status check failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Status check failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
// Notification test functions
function testNotification() {
console.log('testNotification called');
// Quick sanity check - test plugin availability
if (window.Capacitor && window.Capacitor.isPluginAvailable) {
const isAvailable = window.Capacitor.isPluginAvailable('DailyNotification');
console.log('is plugin available?', isAvailable);
}
const status = document.getElementById('status');
status.innerHTML = 'Testing plugin connection...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
// Test the notification method directly
console.log('Testing notification scheduling...');
const now = new Date();
const notificationTime = new Date(now.getTime() + 600000); // 10 minutes from now
const prefetchTime = new Date(now.getTime() + 300000); // 5 minutes from now
const notificationTimeString = notificationTime.getHours().toString().padStart(2, '0') + ':' +
notificationTime.getMinutes().toString().padStart(2, '0');
const prefetchTimeString = prefetchTime.getHours().toString().padStart(2, '0') + ':' +
prefetchTime.getMinutes().toString().padStart(2, '0');
window.DailyNotification.scheduleDailyNotification({
time: notificationTimeString,
title: 'Test Notification',
body: 'This is a test notification from the DailyNotification plugin!',
sound: true,
priority: 'high'
})
.then(() => {
const prefetchTimeReadable = prefetchTime.toLocaleTimeString();
const notificationTimeReadable = notificationTime.toLocaleTimeString();
status.innerHTML = '✅ Notification scheduled!<br>' +
'📥 Prefetch: ' + prefetchTimeReadable + ' (' + prefetchTimeString + ')<br>' +
'🔔 Notification: ' + notificationTimeReadable + ' (' + notificationTimeString + ')';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
})
.catch(error => {
status.innerHTML = `Notification failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Notification test failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
function scheduleNotification() {
console.log('scheduleNotification called');
const status = document.getElementById('status');
status.innerHTML = 'Scheduling notification...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
// Schedule notification for 10 minutes from now (allows 5 min prefetch to fire)
const now = new Date();
const notificationTime = new Date(now.getTime() + 600000); // 10 minutes from now
const prefetchTime = new Date(now.getTime() + 300000); // 5 minutes from now
const notificationTimeString = notificationTime.getHours().toString().padStart(2, '0') + ':' +
notificationTime.getMinutes().toString().padStart(2, '0');
const prefetchTimeString = prefetchTime.getHours().toString().padStart(2, '0') + ':' +
prefetchTime.getMinutes().toString().padStart(2, '0');
window.DailyNotification.scheduleDailyNotification({
time: notificationTimeString,
title: 'Scheduled Notification',
body: 'This notification was scheduled 10 minutes ago!',
sound: true,
priority: 'default'
})
.then(() => {
const prefetchTimeReadable = prefetchTime.toLocaleTimeString();
const notificationTimeReadable = notificationTime.toLocaleTimeString();
status.innerHTML = '✅ Notification scheduled!<br>' +
'📥 Prefetch: ' + prefetchTimeReadable + ' (' + prefetchTimeString + ')<br>' +
'🔔 Notification: ' + notificationTimeReadable + ' (' + notificationTimeString + ')';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
})
.catch(error => {
status.innerHTML = `Scheduling failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Scheduling test failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
function showReminder() {
console.log('showReminder called');
const status = document.getElementById('status');
status.innerHTML = 'Showing reminder...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
// Schedule daily reminder using scheduleDailyReminder
const now = new Date();
const reminderTime = new Date(now.getTime() + 10000); // 10 seconds from now
const timeString = reminderTime.getHours().toString().padStart(2, '0') + ':' +
reminderTime.getMinutes().toString().padStart(2, '0');
window.DailyNotification.scheduleDailyReminder({
id: 'daily-reminder-test',
title: 'Daily Reminder',
body: 'Don\'t forget to check your daily notifications!',
time: timeString,
sound: true,
vibration: true,
priority: 'default',
repeatDaily: false // Just for testing
})
.then(() => {
status.innerHTML = 'Daily reminder scheduled for ' + timeString + '!';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
})
.catch(error => {
status.innerHTML = `Reminder failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Reminder test failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
// Permission management functions
function checkPermissions() {
console.log('checkPermissions called');
const status = document.getElementById('status');
status.innerHTML = 'Checking permissions...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
window.DailyNotification.checkPermissionStatus()
.then(result => {
status.innerHTML = `Permission Status:<br>
Notifications: ${result.notificationsEnabled ? '✅' : '❌'}<br>
Exact Alarm: ${result.exactAlarmEnabled ? '✅' : '❌'}<br>
Wake Lock: ${result.wakeLockEnabled ? '✅' : '❌'}<br>
All Granted: ${result.allPermissionsGranted ? '✅' : '❌'}`;
status.style.background = result.allPermissionsGranted ?
'rgba(0, 255, 0, 0.3)' : 'rgba(255, 165, 0, 0.3)'; // Green or orange
})
.catch(error => {
status.innerHTML = `Permission check failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Permission check failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
function requestPermissions() {
console.log('requestPermissions called');
const status = document.getElementById('status');
status.innerHTML = 'Requesting permissions...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
window.DailyNotification.requestNotificationPermissions()
.then(() => {
status.innerHTML = 'Permission request completed! Check your device settings if needed.';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
// Check permissions again after request
setTimeout(() => {
checkPermissions();
}, 1000);
})
.catch(error => {
status.innerHTML = `Permission request failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Permission request failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
function openExactAlarmSettings() {
console.log('openExactAlarmSettings called');
const status = document.getElementById('status');
status.innerHTML = 'Opening exact alarm settings...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
window.DailyNotification.openExactAlarmSettings()
.then(() => {
status.innerHTML = 'Exact alarm settings opened! Please enable "Allow exact alarms" and return to the app.';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
})
.catch(error => {
status.innerHTML = `Failed to open exact alarm settings: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Failed to open exact alarm settings: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
function checkChannelStatus() {
const status = document.getElementById('status');
status.innerHTML = 'Checking channel status...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
window.DailyNotification.isChannelEnabled()
.then(result => {
const importanceText = getImportanceText(result.importance);
status.innerHTML = `Channel Status: ${result.enabled ? 'Enabled' : 'Disabled'} (${importanceText})`;
status.style.background = result.enabled ? 'rgba(0, 255, 0, 0.3)' : 'rgba(255, 0, 0, 0.3)';
})
.catch(error => {
status.innerHTML = `Channel check failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Channel check failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
function openChannelSettings() {
const status = document.getElementById('status');
status.innerHTML = 'Opening channel settings...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
window.DailyNotification.openChannelSettings()
.then(result => {
if (result.opened) {
status.innerHTML = 'Channel settings opened! Please enable notifications and return to the app.';
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
} else {
status.innerHTML = 'Could not open channel settings (may not be available on this device)';
status.style.background = 'rgba(255, 165, 0, 0.3)'; // Orange background
}
})
.catch(error => {
status.innerHTML = `Failed to open channel settings: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Failed to open channel settings: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
function checkComprehensiveStatus() {
const status = document.getElementById('status');
status.innerHTML = 'Checking comprehensive status...';
status.style.background = 'rgba(255, 255, 0, 0.3)'; // Yellow background
try {
if (!window.DailyNotification) {
status.innerHTML = 'DailyNotification plugin not available';
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
return;
}
window.DailyNotification.checkStatus()
.then(result => {
const canSchedule = result.canScheduleNow;
const issues = [];
if (!result.postNotificationsGranted) {
issues.push('POST_NOTIFICATIONS permission');
}
if (!result.channelEnabled) {
issues.push('notification channel disabled');
}
if (!result.exactAlarmsGranted) {
issues.push('exact alarm permission');
}
let statusText = `Status: ${canSchedule ? 'Ready to schedule' : 'Issues found'}`;
if (issues.length > 0) {
statusText += `\nIssues: ${issues.join(', ')}`;
}
statusText += `\nChannel: ${getImportanceText(result.channelImportance)}`;
statusText += `\nChannel ID: ${result.channelId}`;
status.innerHTML = statusText;
status.style.background = canSchedule ? 'rgba(0, 255, 0, 0.3)' : 'rgba(255, 0, 0, 0.3)';
})
.catch(error => {
status.innerHTML = `Status check failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
});
} catch (error) {
status.innerHTML = `Status check failed: ${error.message}`;
status.style.background = 'rgba(255, 0, 0, 0.3)'; // Red background
}
}
function getImportanceText(importance) {
switch (importance) {
case 0: return 'None (blocked)';
case 1: return 'Min';
case 2: return 'Low';
case 3: return 'Default';
case 4: return 'High';
case 5: return 'Max';
default: return `Unknown (${importance})`;
}
}
// Attach to window object
window.testPlugin = testPlugin;
window.configurePlugin = configurePlugin;
window.checkStatus = checkStatus;
window.testNotification = testNotification;
window.scheduleNotification = scheduleNotification;
window.showReminder = showReminder;
window.checkPermissions = checkPermissions;
window.requestPermissions = requestPermissions;
window.openExactAlarmSettings = openExactAlarmSettings;
window.checkChannelStatus = checkChannelStatus;
window.openChannelSettings = openChannelSettings;
window.checkComprehensiveStatus = checkComprehensiveStatus;
console.log('Functions attached to window:', {
testPlugin: typeof window.testPlugin,
configurePlugin: typeof window.configurePlugin,
checkStatus: typeof window.checkStatus,
testNotification: typeof window.testNotification,
scheduleNotification: typeof window.scheduleNotification,
showReminder: typeof window.showReminder
});
</script>
</body>
</html>

View File

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

View File

@@ -0,0 +1,97 @@
/**
* DemoNativeFetcher.java
*
* Simple demo implementation of NativeNotificationContentFetcher for the plugin demo app.
* Returns mock notification content for demonstration purposes.
*
* @author Matthew Raymer
* @version 1.0.0
*/
package com.timesafari.dailynotification;
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;
/**
* Demo implementation of native content fetcher
*
* Returns mock notification content for demonstration. In production host apps,
* this would fetch from TimeSafari API or other data sources.
*
* <p><b>Configuration:</b></p>
* <p>This fetcher does NOT override {@code configure()} because it uses hardcoded
* mock data and doesn't need API credentials. The default no-op implementation
* from the interface is sufficient.</p>
*
* <p>For an example that accepts configuration from TypeScript, see
* {@code TestNativeFetcher} in the test app.</p>
*/
public class DemoNativeFetcher implements NativeNotificationContentFetcher {
private static final String TAG = "DemoNativeFetcher";
// Note: We intentionally do NOT override configure() because this fetcher
// uses hardcoded mock data. The default no-op implementation from the
// interface is sufficient. This demonstrates that configure() is optional.
@Override
@NonNull
public CompletableFuture<List<NotificationContent>> fetchContent(
@NonNull FetchContext context) {
Log.d(TAG, "DemoNativeFetcher: Fetch triggered - trigger=" + context.trigger +
", scheduledTime=" + context.scheduledTime + ", fetchTime=" + context.fetchTime);
return CompletableFuture.supplyAsync(() -> {
try {
// Simulate network delay
Thread.sleep(100);
List<NotificationContent> contents = new ArrayList<>();
// Create a demo notification
NotificationContent demoContent = new NotificationContent();
demoContent.setId("demo_notification_" + System.currentTimeMillis());
demoContent.setTitle("Demo Notification from Native Fetcher");
demoContent.setBody("This is a demo notification from the native fetcher SPI. " +
"Trigger: " + context.trigger +
(context.scheduledTime != null ?
", Scheduled: " + new java.util.Date(context.scheduledTime) : ""));
// Use scheduled time from context, or default to 1 hour from now
long scheduledTimeMs = context.scheduledTime != null ?
context.scheduledTime : (System.currentTimeMillis() + 3600000);
demoContent.setScheduledTime(scheduledTimeMs);
// fetchTime is set automatically by NotificationContent constructor (as fetchedAt)
demoContent.setPriority("default");
demoContent.setSound(true);
contents.add(demoContent);
Log.i(TAG, "DemoNativeFetcher: Returning " + contents.size() +
" notification(s)");
return contents;
} catch (InterruptedException e) {
Log.e(TAG, "DemoNativeFetcher: Interrupted during fetch", e);
Thread.currentThread().interrupt();
return Collections.emptyList();
} catch (Exception e) {
Log.e(TAG, "DemoNativeFetcher: Error during fetch", e);
return Collections.emptyList();
}
});
}
}

View File

@@ -0,0 +1,11 @@
package com.timesafari.dailynotification;
import com.getcapacitor.BridgeActivity;
import android.os.Bundle;
public class MainActivity extends BridgeActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

View File

@@ -0,0 +1,38 @@
/**
* PluginApplication.java
*
* Application class for the Daily Notification Plugin demo app.
* Registers the native content fetcher SPI implementation.
*
* @author Matthew Raymer
* @version 1.0.0
*/
package com.timesafari.dailynotification;
import android.app.Application;
import android.util.Log;
import com.timesafari.dailynotification.DailyNotificationPlugin;
import com.timesafari.dailynotification.NativeNotificationContentFetcher;
/**
* Application class that registers native fetcher for plugin demo app
*/
public class PluginApplication extends Application {
private static final String TAG = "PluginApplication";
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "Initializing Daily Notification Plugin demo app");
// Register demo native fetcher
NativeNotificationContentFetcher demoFetcher = new DemoNativeFetcher();
DailyNotificationPlugin.setNativeFetcher(demoFetcher);
Log.i(TAG, "Demo native fetcher registered: " + demoFetcher.getClass().getName());
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">DailyNotificationPlugin</string>
<string name="title_activity_main">DailyNotificationPlugin</string>
<string name="package_name">com.timesafari.dailynotification</string>
<string name="custom_url_scheme">com.timesafari.dailynotification</string>
</resources>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<access origin="*" />
</widget>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeSafari Daily Notification Plugin - Notification Channels Configuration
Defines notification channels for different types of TimeSafari notifications
with appropriate importance levels and user control options.
@author Matthew Raymer
@version 1.0.0
-->
<resources>
<!-- TimeSafari Community Updates Channel -->
<string name="channel_community_id">timesafari_community_updates</string>
<string name="channel_community_name">TimeSafari Community Updates</string>
<string name="channel_community_description">Daily updates from your TimeSafari community including new offers, project updates, and trust network activities</string>
<!-- TimeSafari Project Notifications Channel -->
<string name="channel_projects_id">timesafari_project_notifications</string>
<string name="channel_projects_name">TimeSafari Project Notifications</string>
<string name="channel_projects_description">Notifications about starred projects, funding updates, and project milestones</string>
<!-- TimeSafari Trust Network Channel -->
<string name="channel_trust_id">timesafari_trust_network</string>
<string name="channel_trust_name">TimeSafari Trust Network</string>
<string name="channel_trust_description">Trust network activities, endorsements, and community recommendations</string>
<!-- TimeSafari System Notifications Channel -->
<string name="channel_system_id">timesafari_system</string>
<string name="channel_system_name">TimeSafari System</string>
<string name="channel_system_description">System notifications, authentication updates, and plugin status messages</string>
<!-- TimeSafari Reminders Channel -->
<string name="channel_reminders_id">timesafari_reminders</string>
<string name="channel_reminders_name">TimeSafari Reminders</string>
<string name="channel_reminders_description">Personal reminders and daily check-ins for your TimeSafari activities</string>
</resources>

View File

@@ -0,0 +1,18 @@
package com.timesafari.dailynotification;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View File

@@ -0,0 +1,25 @@
// Root build file for Android test app
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.0'
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
android.enableJetifier=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
test-apps/android-test-app/gradlew vendored Executable file
View File

@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
test-apps/android-test-app/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,27 @@
include ':app'
// Note: capacitor-cordova-android-plugins is not needed for standalone Android test app
// It's only generated by Capacitor CLI for Capacitor apps
// Include Capacitor Android (required for plugin)
// Try to find Capacitor from the Vue test app's node_modules
def capacitorPath = new File(settingsDir, '../daily-notification-test/node_modules/@capacitor/android/capacitor')
if (capacitorPath.exists()) {
include ':capacitor-android'
project(':capacitor-android').projectDir = capacitorPath
} else {
throw new GradleException("Capacitor not found at ${capacitorPath.absolutePath}. Please run 'npm install' in test-apps/daily-notification-test/ first.")
}
// Reference plugin from parent directory (for local development)
// Path: test-apps/android-test-app/../../android = root/android
// settingsDir = test-apps/android-test-app/
// settingsDir.parentFile = test-apps/
// settingsDir.parentFile.parentFile = root project directory
def pluginPath = new File(settingsDir.parentFile.parentFile, 'android')
if (pluginPath.exists() && new File(pluginPath, 'build.gradle').exists()) {
include ':daily-notification-plugin'
project(':daily-notification-plugin').projectDir = pluginPath
} else {
throw new GradleException("Plugin not found at ${pluginPath.absolutePath}. Please ensure the plugin is at the correct location.")
}

View File

@@ -0,0 +1,16 @@
ext {
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.12.1'
junitVersion = '4.13.2'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
}

View File

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

View File

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