diff --git a/BUILDING.md b/BUILDING.md index 8e4015a..f83ea90 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -192,7 +192,7 @@ Build → Generate Signed Bundle / APK #### Build Output The built plugin AAR will be located at: ``` -android/plugin/build/outputs/aar/plugin-release.aar +android/build/outputs/aar/android-release.aar ``` ### Project Structure in Android Studio @@ -226,36 +226,14 @@ android/ ### Important Distinctions -#### Plugin Module (`android/plugin/`) - -- **Purpose**: Contains the actual plugin code -- **No MainActivity** - This is a library, not an app -- **No UI Components** - Plugins provide functionality to host apps -- **Output**: AAR library files - -#### Test App Module (`android/app/`) - -- **Purpose**: Test application for the plugin -- **Has MainActivity** - Full Capacitor app with BridgeActivity -- **Has UI Components** - HTML/JS interface for testing -- **Output**: APK files for installation - -#### What You CAN Do in Android Studio -✅ **Edit Java/Kotlin code** (both plugin and app) -✅ **Run unit tests** (both modules) -✅ **Debug plugin code** (plugin module) -✅ **Build the plugin AAR** (plugin module) -✅ **Build test app APK** (app module) -✅ **Run the test app** (app module) -✅ **Test notifications** (app module) -✅ **Test background tasks** (app module) -✅ **Debug full integration** (app module) -✅ **Check for compilation errors** -✅ **Use code completion and refactoring** -✅ **View build logs and errors** - -#### What You CANNOT Do -❌ **Run plugin module directly** (it's a library) +#### Standard Capacitor Plugin Structure + +The plugin now follows the standard Capacitor Android structure: +- **Plugin Code**: `android/src/main/java/...` +- **Plugin Build**: `android/build.gradle` +- **Test App**: `test-apps/android-test-app/app/` (separate from plugin) + +This structure is compatible with Capacitor's auto-generated files and requires no path fixes. ❌ **Test plugin without host app** (needs Capacitor runtime) ## Command Line Building @@ -416,12 +394,12 @@ test-apps/daily-notification-test/ #### Android Test Apps The project includes **two separate Android test applications**: -##### 1. Main Android Test App (`/android/app`) +##### 1. Main Android Test App (`test-apps/android-test-app/app`) A Capacitor-based Android test app with full plugin integration: ```bash # Build main Android test app -cd android +cd test-apps/android-test-app ./gradlew :app:assembleDebug # Install on device @@ -431,37 +409,30 @@ adb install app/build/outputs/apk/debug/app-debug.apk ./gradlew :app:test # Run in Android Studio -# File → Open → /path/to/daily-notification-plugin/android +# File → Open → /path/to/daily-notification-plugin/test-apps/android-test-app # Select 'app' module and run ``` -**App Structure:** -``` -android/app/ -├── src/ -│ ├── main/ -│ │ ├── AndroidManifest.xml # App manifest with permissions -│ │ ├── assets/ # Capacitor web assets -│ │ │ ├── capacitor.config.json # Capacitor configuration -│ │ │ ├── capacitor.plugins.json # Plugin registry -│ │ │ └── public/ # Web app files -│ │ │ ├── index.html # Main test interface -│ │ │ ├── cordova.js # Cordova compatibility -│ │ │ └── plugins/ # Plugin JS files -│ │ ├── java/ -│ │ │ └── com/timesafari/dailynotification/ -│ │ │ └── MainActivity.java # Capacitor BridgeActivity -│ │ └── res/ # Android resources -│ │ ├── drawable/ # App icons and images -│ │ ├── layout/ # Android layouts -│ │ ├── mipmap/ # App launcher icons -│ │ ├── values/ # Strings, styles, colors -│ │ └── xml/ # Configuration files -│ ├── androidTest/ # Instrumented tests -│ └── test/ # Unit tests -├── build.gradle # App build configuration -├── capacitor.build.gradle # Auto-generated Capacitor config -└── proguard-rules.pro # Code obfuscation rules +**Test App Structure:** +``` +test-apps/android-test-app/ +├── app/ +│ ├── src/ +│ │ ├── main/ +│ │ │ ├── AndroidManifest.xml # App manifest with permissions +│ │ │ ├── assets/ # Capacitor web assets +│ │ │ │ ├── capacitor.config.json # Capacitor configuration +│ │ │ │ ├── capacitor.plugins.json # Plugin registry +│ │ │ │ └── public/ # Web app files +│ │ │ ├── java/ +│ │ │ │ └── com/timesafari/dailynotification/ +│ │ │ │ └── MainActivity.java # Capacitor BridgeActivity +│ │ │ └── res/ # Android resources +│ │ ├── androidTest/ # Instrumented tests +│ │ └── test/ # Unit tests +│ ├── build.gradle # App build configuration +│ ├── capacitor.build.gradle # Auto-generated Capacitor config +│ └── proguard-rules.pro # Code obfuscation rules ``` **Key Files Explained:** @@ -504,7 +475,7 @@ public class MainActivity extends BridgeActivity { 2. **Java Compilation**: Compiles `MainActivity.java` and dependencies 3. **Resource Processing**: Processes Android resources and assets 4. **APK Generation**: Packages everything into installable APK -5. **Plugin Integration**: Links with plugin AAR from `android/plugin/` +5. **Plugin Integration**: Links with plugin from `node_modules/@timesafari/daily-notification-plugin/android` **Editing Guidelines:** - **HTML/JS**: Edit `assets/public/index.html` for UI changes @@ -547,7 +518,7 @@ The Vue 3 test app uses a **project reference approach** for plugin integration: ```gradle // capacitor.settings.gradle include ':timesafari-daily-notification-plugin' - project(':timesafari-daily-notification-plugin').projectDir = new File('../../../android/plugin') + project(':timesafari-daily-notification-plugin').projectDir = new File('../../../android') // capacitor.build.gradle implementation project(':timesafari-daily-notification-plugin') @@ -576,7 +547,7 @@ The Vue 3 test app uses a **project reference approach** for plugin integration: **Troubleshooting Integration Issues:** - **Duplicate Classes**: Use project reference instead of AAR to avoid conflicts - **Gradle Cache**: Clear completely (`rm -rf ~/.gradle`) when switching approaches -- **Path Issues**: Ensure correct project path (`../../../android/plugin`) +- **Path Issues**: Ensure correct project path (`../../../android`) - **Dependencies**: Include required WorkManager and Gson dependencies ### Integration Testing @@ -881,33 +852,7 @@ rm -rf ~/.gradle/caches ~/.gradle/daemon #### Capacitor Settings Path Fix (Test App) -**Problem**: `capacitor.settings.gradle` is auto-generated with incorrect plugin path. -The plugin module is in `android/plugin/` but Capacitor generates a path to `android/`. - -**Automatic Solution** (Test App Only): -```bash -# Use the wrapper script that auto-fixes after sync: -npm run cap:sync - -# This automatically: -# 1. Runs npx cap sync android -# 2. Fixes capacitor.settings.gradle path (android -> android/plugin/) -# 3. Fixes capacitor.plugins.json registration -``` - -**Manual Fix** (if needed): -```bash -# After running npx cap sync android directly: -node scripts/fix-capacitor-plugins.js - -# Or for plugin development (root project): -./scripts/fix-capacitor-build.sh -``` - -**Automatic Fix on Install**: -The test app has a `postinstall` hook that automatically fixes these issues after `npm install`. - -**Note**: The fix script is idempotent - it only changes what's needed and won't break correct configurations. +**Note**: The plugin now uses standard Capacitor structure, so no path fixes are needed for consuming apps. The test app at `test-apps/android-test-app/` references the plugin correctly. #### Android Studio Issues ```bash @@ -1031,19 +976,9 @@ daily-notification-plugin/ ### Android Structure ``` android/ -├── app/ # Main Android test app -│ ├── src/main/java/ # MainActivity.java -│ ├── src/main/assets/ # Capacitor assets -│ ├── build.gradle # App build configuration -│ └── build/outputs/apk/ # Built APK files -├── plugin/ # Plugin library module -│ ├── src/main/java/ # Plugin source code -│ ├── build.gradle # Plugin build configuration -│ └── build/outputs/aar/ # Built AAR files -├── build.gradle # Root Android build configuration -├── settings.gradle # Gradle settings -├── gradle.properties # Gradle properties -└── gradle/wrapper/ # Gradle wrapper files +├── src/main/java/ # Plugin source code +├── build.gradle # Plugin build configuration +└── variables.gradle # Gradle variables ``` ### iOS Structure diff --git a/INTEGRATION_GUIDE.md b/INTEGRATION_GUIDE.md index a530878..9bcf5e3 100644 --- a/INTEGRATION_GUIDE.md +++ b/INTEGRATION_GUIDE.md @@ -85,16 +85,16 @@ The plugin has been optimized for **native-first deployment** with the following ## Plugin Repository Structure -The TimeSafari Daily Notification Plugin follows this structure: +The TimeSafari Daily Notification Plugin follows the standard Capacitor plugin structure: ``` daily-notification-plugin/ ├── android/ -│ ├── build.gradle +│ ├── build.gradle # Plugin build configuration │ ├── src/main/java/com/timesafari/dailynotification/ │ │ ├── DailyNotificationPlugin.java -│ │ ├── NotificationWorker.java -│ │ ├── DatabaseManager.java -│ │ └── CallbackRegistry.java +│ │ ├── DailyNotificationWorker.java +│ │ ├── DailyNotificationDatabase.java +│ │ └── ... (other plugin classes) │ └── src/main/AndroidManifest.xml ├── ios/ │ ├── DailyNotificationPlugin.swift diff --git a/README.md b/README.md index d2ee5ea..cb3e437 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,14 @@ The plugin has been optimized for **native-first deployment** with the following npm install @timesafari/daily-notification-plugin ``` +Or install from Git repository: + +```bash +npm install git+https://github.com/timesafari/daily-notification-plugin.git +``` + +The plugin follows the standard Capacitor Android structure - no additional path configuration needed! + ## Quick Start ### Basic Usage diff --git a/android/.gitignore b/android/.gitignore index 48354a3..2ede56d 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -16,13 +16,17 @@ bin/ gen/ out/ -# Uncomment the following line in case you need and you don't have the release build type files in your app -# release/ # Gradle files .gradle/ build/ +# Keep gradle wrapper files - they're needed for builds +!gradle/wrapper/gradle-wrapper.jar +!gradle/wrapper/gradle-wrapper.properties +!gradlew +!gradlew.bat + # Local configuration file (sdk path, etc) local.properties @@ -38,19 +42,9 @@ proguard/ # Android Studio captures folder captures/ -# IntelliJ +# IntelliJ / Android Studio *.iml -.idea/workspace.xml -.idea/tasks.xml -.idea/gradle.xml -.idea/assetWizardSettings.xml -.idea/dictionaries -.idea/libraries -# Android Studio 3 in .gitignore file. -.idea/caches -.idea/modules.xml -# Comment next line if keeping position of elements in Navigation Editor is relevant for you -.idea/navEditor.xml +.idea/ # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. @@ -64,38 +58,6 @@ captures/ # Google Services (e.g. APIs or Firebase) # google-services.json -# Freeline -freeline.py -freeline/ -freeline_project_description.json - -# fastlane -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots -fastlane/test_output -fastlane/readme.md - -# Version control -vcs.xml - -# lint -lint/intermediates/ -lint/generated/ -lint/outputs/ -lint/tmp/ -# lint/reports/ - # Android Profiling *.hprof -# Cordova plugins for Capacitor -capacitor-cordova-android-plugins - -# Copied web assets -app/src/main/assets/public - -# Generated Config files -app/src/main/assets/capacitor.config.json -app/src/main/assets/capacitor.plugins.json -app/src/main/res/xml/config.xml diff --git a/android/BUILDING.md b/android/BUILDING.md new file mode 100644 index 0000000..2e00de5 --- /dev/null +++ b/android/BUILDING.md @@ -0,0 +1,69 @@ +# Building the Daily Notification Plugin + +## Important: Standalone Build Limitations + +**Capacitor plugins cannot be built standalone** because Capacitor dependencies are npm packages, not Maven artifacts. + +### ✅ Correct Way to Build + +Build the plugin **within a Capacitor app** that uses it: + +```bash +# In a consuming Capacitor app (e.g., test-apps/android-test-app or your app) +cd /path/to/capacitor-app/android +./gradlew assembleDebug + +# Or use Capacitor CLI +npx cap sync android +npx cap run android +``` + +### ❌ What Doesn't Work + +```bash +# This will fail - Capacitor dependencies aren't in Maven +cd android +./gradlew assembleDebug +``` + +### Why This Happens + +1. **Capacitor dependencies are npm packages**, not Maven artifacts +2. **Capacitor plugins are meant to be consumed**, not built standalone +3. **The consuming app provides Capacitor** as a project dependency +4. **When you run `npx cap sync`**, Capacitor sets up the correct dependency structure + +### For Development & Testing + +Use the test app at `test-apps/android-test-app/`: + +```bash +cd test-apps/android-test-app +npm install +npx cap sync android +cd android +./gradlew assembleDebug +``` + +The plugin will be built as part of the test app's build process. + +### Gradle Wrapper Purpose + +The gradle wrapper in `android/` is provided for: +- ✅ **Syntax checking** - Verify build.gradle syntax +- ✅ **Android Studio** - Open the plugin directory in Android Studio for editing +- ✅ **Documentation** - Show available tasks and structure +- ❌ **Not for standalone builds** - Requires a consuming app context + +### Verifying Build Configuration + +You can verify the build configuration is correct: + +```bash +cd android +./gradlew tasks # Lists available tasks (may show dependency errors, that's OK) +./gradlew clean # Cleans build directory +``` + +The dependency errors are expected - they confirm the plugin needs a consuming app context. + diff --git a/android/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.kt b/android/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.kt deleted file mode 100644 index 8d19f1c..0000000 --- a/android/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.timesafari.dailynotification - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -/** - * Boot recovery receiver to reschedule notifications after device reboot - * Implements RECEIVE_BOOT_COMPLETED functionality - * - * @author Matthew Raymer - * @version 1.1.0 - */ -class BootReceiver : BroadcastReceiver() { - - companion object { - private const val TAG = "DNP-BOOT" - } - - override fun onReceive(context: Context, intent: Intent?) { - if (intent?.action == Intent.ACTION_BOOT_COMPLETED) { - Log.i(TAG, "Boot completed, rescheduling notifications") - - CoroutineScope(Dispatchers.IO).launch { - try { - rescheduleNotifications(context) - } catch (e: Exception) { - Log.e(TAG, "Failed to reschedule notifications after boot", e) - } - } - } - } - - private suspend fun rescheduleNotifications(context: Context) { - val db = DailyNotificationDatabase.getDatabase(context) - val enabledSchedules = db.scheduleDao().getEnabled() - - Log.i(TAG, "Found ${enabledSchedules.size} enabled schedules to reschedule") - - enabledSchedules.forEach { schedule -> - try { - when (schedule.kind) { - "fetch" -> { - // Reschedule WorkManager fetch - val config = ContentFetchConfig( - enabled = schedule.enabled, - schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *", - url = null, // Will use mock content - timeout = 30000, - retryAttempts = 3, - retryDelay = 1000, - callbacks = CallbackConfig() - ) - FetchWorker.scheduleFetch(context, config) - Log.i(TAG, "Rescheduled fetch for schedule: ${schedule.id}") - } - "notify" -> { - // Reschedule AlarmManager notification - val nextRunTime = calculateNextRunTime(schedule) - if (nextRunTime > System.currentTimeMillis()) { - val config = UserNotificationConfig( - enabled = schedule.enabled, - schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *", - title = "Daily Notification", - body = "Your daily update is ready", - sound = true, - vibration = true, - priority = "normal" - ) - NotifyReceiver.scheduleExactNotification(context, nextRunTime, config) - Log.i(TAG, "Rescheduled notification for schedule: ${schedule.id}") - } - } - else -> { - Log.w(TAG, "Unknown schedule kind: ${schedule.kind}") - } - } - } catch (e: Exception) { - Log.e(TAG, "Failed to reschedule ${schedule.kind} for ${schedule.id}", e) - } - } - - // Record boot recovery in history - try { - db.historyDao().insert( - History( - refId = "boot_recovery_${System.currentTimeMillis()}", - kind = "boot_recovery", - occurredAt = System.currentTimeMillis(), - outcome = "success", - diagJson = "{\"schedules_rescheduled\": ${enabledSchedules.size}}" - ) - ) - } catch (e: Exception) { - Log.e(TAG, "Failed to record boot recovery", e) - } - } - - private fun calculateNextRunTime(schedule: Schedule): Long { - val now = System.currentTimeMillis() - - // Simple implementation - for production, use proper cron parsing - return when { - schedule.cron != null -> { - // Parse cron expression and calculate next run - // For now, return next day at 9 AM - now + (24 * 60 * 60 * 1000L) - } - schedule.clockTime != null -> { - // Parse HH:mm and calculate next run - // For now, return next day at specified time - now + (24 * 60 * 60 * 1000L) - } - else -> { - // Default to next day at 9 AM - now + (24 * 60 * 60 * 1000L) - } - } - } -} - -/** - * Data classes for configuration (simplified versions) - */ -data class ContentFetchConfig( - val enabled: Boolean, - val schedule: String, - val url: String? = null, - val timeout: Int? = null, - val retryAttempts: Int? = null, - val retryDelay: Int? = null, - val callbacks: CallbackConfig -) - -data class UserNotificationConfig( - val enabled: Boolean, - val schedule: String, - val title: String? = null, - val body: String? = null, - val sound: Boolean? = null, - val vibration: Boolean? = null, - val priority: String? = null -) - -data class CallbackConfig( - val apiService: String? = null, - val database: String? = null, - val reporting: String? = null -) diff --git a/android/android/plugin/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt b/android/android/plugin/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt deleted file mode 100644 index cda440c..0000000 --- a/android/android/plugin/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.timesafari.dailynotification - -import androidx.room.* -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -/** - * SQLite schema for Daily Notification Plugin - * Implements TTL-at-fire invariant and rolling window armed design - * - * @author Matthew Raymer - * @version 1.1.0 - */ -@Entity(tableName = "content_cache") -data class ContentCache( - @PrimaryKey val id: String, - val fetchedAt: Long, // epoch ms - val ttlSeconds: Int, - val payload: ByteArray, // BLOB - val meta: String? = null -) - -@Entity(tableName = "schedules") -data class Schedule( - @PrimaryKey val id: String, - val kind: String, // 'fetch' or 'notify' - val cron: String? = null, // optional cron expression - val clockTime: String? = null, // optional HH:mm - val enabled: Boolean = true, - val lastRunAt: Long? = null, - val nextRunAt: Long? = null, - val jitterMs: Int = 0, - val backoffPolicy: String = "exp", - val stateJson: String? = null -) - -@Entity(tableName = "callbacks") -data class Callback( - @PrimaryKey val id: String, - val kind: String, // 'http', 'local', 'queue' - val target: String, // url_or_local - val headersJson: String? = null, - val enabled: Boolean = true, - val createdAt: Long -) - -@Entity(tableName = "history") -data class History( - @PrimaryKey(autoGenerate = true) val id: Int = 0, - val refId: String, // content or schedule id - val kind: String, // fetch/notify/callback - val occurredAt: Long, - val durationMs: Long? = null, - val outcome: String, // success|failure|skipped_ttl|circuit_open - val diagJson: String? = null -) - -@Database( - entities = [ContentCache::class, Schedule::class, Callback::class, History::class], - version = 1, - exportSchema = false -) -@TypeConverters(Converters::class) -abstract class DailyNotificationDatabase : RoomDatabase() { - abstract fun contentCacheDao(): ContentCacheDao - abstract fun scheduleDao(): ScheduleDao - abstract fun callbackDao(): CallbackDao - abstract fun historyDao(): HistoryDao -} - -@Dao -interface ContentCacheDao { - @Query("SELECT * FROM content_cache WHERE id = :id") - suspend fun getById(id: String): ContentCache? - - @Query("SELECT * FROM content_cache ORDER BY fetchedAt DESC LIMIT 1") - suspend fun getLatest(): ContentCache? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(contentCache: ContentCache) - - @Query("DELETE FROM content_cache WHERE fetchedAt < :cutoffTime") - suspend fun deleteOlderThan(cutoffTime: Long) - - @Query("SELECT COUNT(*) FROM content_cache") - suspend fun getCount(): Int -} - -@Dao -interface ScheduleDao { - @Query("SELECT * FROM schedules WHERE enabled = 1") - suspend fun getEnabled(): List - - @Query("SELECT * FROM schedules WHERE id = :id") - suspend fun getById(id: String): Schedule? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(schedule: Schedule) - - @Query("UPDATE schedules SET enabled = :enabled WHERE id = :id") - suspend fun setEnabled(id: String, enabled: Boolean) - - @Query("UPDATE schedules SET lastRunAt = :lastRunAt, nextRunAt = :nextRunAt WHERE id = :id") - suspend fun updateRunTimes(id: String, lastRunAt: Long?, nextRunAt: Long?) -} - -@Dao -interface CallbackDao { - @Query("SELECT * FROM callbacks WHERE enabled = 1") - suspend fun getEnabled(): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(callback: Callback) - - @Query("DELETE FROM callbacks WHERE id = :id") - suspend fun deleteById(id: String) -} - -@Dao -interface HistoryDao { - @Insert - suspend fun insert(history: History) - - @Query("SELECT * FROM history WHERE occurredAt >= :since ORDER BY occurredAt DESC") - suspend fun getSince(since: Long): List - - @Query("DELETE FROM history WHERE occurredAt < :cutoffTime") - suspend fun deleteOlderThan(cutoffTime: Long) - - @Query("SELECT COUNT(*) FROM history") - suspend fun getCount(): Int -} - -class Converters { - @TypeConverter - fun fromByteArray(value: ByteArray?): String? { - return value?.let { String(it) } - } - - @TypeConverter - fun toByteArray(value: String?): ByteArray? { - return value?.toByteArray() - } -} diff --git a/android/android/plugin/src/main/java/com/timesafari/dailynotification/FetchWorker.kt b/android/android/plugin/src/main/java/com/timesafari/dailynotification/FetchWorker.kt deleted file mode 100644 index 79e5273..0000000 --- a/android/android/plugin/src/main/java/com/timesafari/dailynotification/FetchWorker.kt +++ /dev/null @@ -1,202 +0,0 @@ -package com.timesafari.dailynotification - -import android.content.Context -import android.util.Log -import androidx.work.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.IOException -import java.net.HttpURLConnection -import java.net.URL -import java.util.concurrent.TimeUnit - -/** - * WorkManager implementation for content fetching - * Implements exponential backoff and network constraints - * - * @author Matthew Raymer - * @version 1.1.0 - */ -class FetchWorker( - appContext: Context, - workerParams: WorkerParameters -) : CoroutineWorker(appContext, workerParams) { - - companion object { - private const val TAG = "DNP-FETCH" - private const val WORK_NAME = "fetch_content" - - fun scheduleFetch(context: Context, config: ContentFetchConfig) { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - val workRequest = OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .setBackoffCriteria( - BackoffPolicy.EXPONENTIAL, - 30, - TimeUnit.SECONDS - ) - .setInputData( - Data.Builder() - .putString("url", config.url) - .putString("headers", config.headers?.toString()) - .putInt("timeout", config.timeout ?: 30000) - .putInt("retryAttempts", config.retryAttempts ?: 3) - .putInt("retryDelay", config.retryDelay ?: 1000) - .build() - ) - .build() - - WorkManager.getInstance(context) - .enqueueUniqueWork( - WORK_NAME, - ExistingWorkPolicy.REPLACE, - workRequest - ) - } - } - - override suspend fun doWork(): Result = withContext(Dispatchers.IO) { - val start = SystemClock.elapsedRealtime() - val url = inputData.getString("url") - val timeout = inputData.getInt("timeout", 30000) - val retryAttempts = inputData.getInt("retryAttempts", 3) - val retryDelay = inputData.getInt("retryDelay", 1000) - - try { - Log.i(TAG, "Starting content fetch from: $url") - - val payload = fetchContent(url, timeout, retryAttempts, retryDelay) - val contentCache = ContentCache( - id = generateId(), - fetchedAt = System.currentTimeMillis(), - ttlSeconds = 3600, // 1 hour default TTL - payload = payload, - meta = "fetched_by_workmanager" - ) - - // Store in database - val db = DailyNotificationDatabase.getDatabase(applicationContext) - db.contentCacheDao().upsert(contentCache) - - // Record success in history - db.historyDao().insert( - History( - refId = contentCache.id, - kind = "fetch", - occurredAt = System.currentTimeMillis(), - durationMs = SystemClock.elapsedRealtime() - start, - outcome = "success" - ) - ) - - Log.i(TAG, "Content fetch completed successfully") - Result.success() - - } catch (e: IOException) { - Log.w(TAG, "Network error during fetch", e) - recordFailure("network_error", start, e) - Result.retry() - - } catch (e: Exception) { - Log.e(TAG, "Unexpected error during fetch", e) - recordFailure("unexpected_error", start, e) - Result.failure() - } - } - - private suspend fun fetchContent( - url: String?, - timeout: Int, - retryAttempts: Int, - retryDelay: Int - ): ByteArray { - if (url.isNullOrBlank()) { - // Generate mock content for testing - return generateMockContent() - } - - var lastException: Exception? = null - - repeat(retryAttempts) { attempt -> - try { - val connection = URL(url).openConnection() as HttpURLConnection - connection.connectTimeout = timeout - connection.readTimeout = timeout - connection.requestMethod = "GET" - - val responseCode = connection.responseCode - if (responseCode == HttpURLConnection.HTTP_OK) { - return connection.inputStream.readBytes() - } else { - throw IOException("HTTP $responseCode: ${connection.responseMessage}") - } - - } catch (e: Exception) { - lastException = e - if (attempt < retryAttempts - 1) { - Log.w(TAG, "Fetch attempt ${attempt + 1} failed, retrying in ${retryDelay}ms", e) - kotlinx.coroutines.delay(retryDelay.toLong()) - } - } - } - - throw lastException ?: IOException("All retry attempts failed") - } - - private fun generateMockContent(): ByteArray { - val mockData = """ - { - "timestamp": ${System.currentTimeMillis()}, - "content": "Daily notification content", - "source": "mock_generator", - "version": "1.1.0" - } - """.trimIndent() - return mockData.toByteArray() - } - - private suspend fun recordFailure(outcome: String, start: Long, error: Throwable) { - try { - val db = DailyNotificationDatabase.getDatabase(applicationContext) - db.historyDao().insert( - History( - refId = "fetch_${System.currentTimeMillis()}", - kind = "fetch", - occurredAt = System.currentTimeMillis(), - durationMs = SystemClock.elapsedRealtime() - start, - outcome = outcome, - diagJson = "{\"error\": \"${error.message}\"}" - ) - ) - } catch (e: Exception) { - Log.e(TAG, "Failed to record failure", e) - } - } - - private fun generateId(): String { - return "fetch_${System.currentTimeMillis()}_${(1000..9999).random()}" - } -} - -/** - * Database singleton for Room - */ -object DailyNotificationDatabase { - @Volatile - private var INSTANCE: DailyNotificationDatabase? = null - - fun getDatabase(context: Context): DailyNotificationDatabase { - return INSTANCE ?: synchronized(this) { - val instance = Room.databaseBuilder( - context.applicationContext, - DailyNotificationDatabase::class.java, - "daily_notification_database" - ).build() - INSTANCE = instance - instance - } - } -} diff --git a/android/android/plugin/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt b/android/android/plugin/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt deleted file mode 100644 index 8998b0c..0000000 --- a/android/android/plugin/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt +++ /dev/null @@ -1,336 +0,0 @@ -package com.timesafari.dailynotification - -import android.app.AlarmManager -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.Build -import android.util.Log -import androidx.core.app.NotificationCompat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -/** - * AlarmManager implementation for user notifications - * Implements TTL-at-fire logic and notification delivery - * - * @author Matthew Raymer - * @version 1.1.0 - */ -class NotifyReceiver : BroadcastReceiver() { - - companion object { - private const val TAG = "DNP-NOTIFY" - private const val CHANNEL_ID = "daily_notifications" - private const val NOTIFICATION_ID = 1001 - private const val REQUEST_CODE = 2001 - - fun scheduleExactNotification( - context: Context, - triggerAtMillis: Long, - config: UserNotificationConfig - ) { - val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - val intent = Intent(context, NotifyReceiver::class.java).apply { - putExtra("title", config.title) - putExtra("body", config.body) - putExtra("sound", config.sound ?: true) - putExtra("vibration", config.vibration ?: true) - putExtra("priority", config.priority ?: "normal") - } - - val pendingIntent = PendingIntent.getBroadcast( - context, - REQUEST_CODE, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - triggerAtMillis, - pendingIntent - ) - } else { - alarmManager.setExact( - AlarmManager.RTC_WAKEUP, - triggerAtMillis, - pendingIntent - ) - } - Log.i(TAG, "Exact notification scheduled for: $triggerAtMillis") - } catch (e: SecurityException) { - Log.w(TAG, "Cannot schedule exact alarm, falling back to inexact", e) - alarmManager.set( - AlarmManager.RTC_WAKEUP, - triggerAtMillis, - pendingIntent - ) - } - } - - fun cancelNotification(context: Context) { - val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - val intent = Intent(context, NotifyReceiver::class.java) - val pendingIntent = PendingIntent.getBroadcast( - context, - REQUEST_CODE, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - alarmManager.cancel(pendingIntent) - Log.i(TAG, "Notification alarm cancelled") - } - } - - override fun onReceive(context: Context, intent: Intent?) { - Log.i(TAG, "Notification receiver triggered") - - CoroutineScope(Dispatchers.IO).launch { - try { - // Check if this is a static reminder (no content dependency) - val isStaticReminder = intent?.getBooleanExtra("is_static_reminder", false) ?: false - - if (isStaticReminder) { - // Handle static reminder without content cache - val title = intent?.getStringExtra("title") ?: "Daily Reminder" - val body = intent?.getStringExtra("body") ?: "Don't forget your daily check-in!" - val sound = intent?.getBooleanExtra("sound", true) ?: true - val vibration = intent?.getBooleanExtra("vibration", true) ?: true - val priority = intent?.getStringExtra("priority") ?: "normal" - val reminderId = intent?.getStringExtra("reminder_id") ?: "unknown" - - showStaticReminderNotification(context, title, body, sound, vibration, priority, reminderId) - - // Record reminder trigger in database - recordReminderTrigger(context, reminderId) - return@launch - } - - // Existing cached content logic for regular notifications - val db = DailyNotificationDatabase.getDatabase(context) - val latestCache = db.contentCacheDao().getLatest() - - if (latestCache == null) { - Log.w(TAG, "No cached content available for notification") - recordHistory(db, "notify", "no_content") - return@launch - } - - // TTL-at-fire check - val now = System.currentTimeMillis() - val ttlExpiry = latestCache.fetchedAt + (latestCache.ttlSeconds * 1000L) - - if (now > ttlExpiry) { - Log.i(TAG, "Content TTL expired, skipping notification") - recordHistory(db, "notify", "skipped_ttl") - return@launch - } - - // Show notification - val title = intent?.getStringExtra("title") ?: "Daily Notification" - val body = intent?.getStringExtra("body") ?: String(latestCache.payload) - val sound = intent?.getBooleanExtra("sound", true) ?: true - val vibration = intent?.getBooleanExtra("vibration", true) ?: true - val priority = intent?.getStringExtra("priority") ?: "normal" - - showNotification(context, title, body, sound, vibration, priority) - recordHistory(db, "notify", "success") - - // Fire callbacks - fireCallbacks(context, db, "onNotifyDelivered", latestCache) - - } catch (e: Exception) { - Log.e(TAG, "Error in notification receiver", e) - try { - val db = DailyNotificationDatabase.getDatabase(context) - recordHistory(db, "notify", "failure", e.message) - } catch (dbError: Exception) { - Log.e(TAG, "Failed to record notification failure", dbError) - } - } - } - } - - private fun showNotification( - context: Context, - title: String, - body: String, - sound: Boolean, - vibration: Boolean, - priority: String - ) { - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - // Create notification channel for Android 8.0+ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - CHANNEL_ID, - "Daily Notifications", - when (priority) { - "high" -> NotificationManager.IMPORTANCE_HIGH - "low" -> NotificationManager.IMPORTANCE_LOW - else -> NotificationManager.IMPORTANCE_DEFAULT - } - ).apply { - enableVibration(vibration) - if (!sound) { - setSound(null, null) - } - } - notificationManager.createNotificationChannel(channel) - } - - val notification = NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle(title) - .setContentText(body) - .setSmallIcon(android.R.drawable.ic_dialog_info) - .setPriority( - when (priority) { - "high" -> NotificationCompat.PRIORITY_HIGH - "low" -> NotificationCompat.PRIORITY_LOW - else -> NotificationCompat.PRIORITY_DEFAULT - } - ) - .setAutoCancel(true) - .setVibrate(if (vibration) longArrayOf(0, 250, 250, 250) else null) - .build() - - notificationManager.notify(NOTIFICATION_ID, notification) - Log.i(TAG, "Notification displayed: $title") - } - - private suspend fun recordHistory( - db: DailyNotificationDatabase, - kind: String, - outcome: String, - diagJson: String? = null - ) { - try { - db.historyDao().insert( - History( - refId = "notify_${System.currentTimeMillis()}", - kind = kind, - occurredAt = System.currentTimeMillis(), - outcome = outcome, - diagJson = diagJson - ) - ) - } catch (e: Exception) { - Log.e(TAG, "Failed to record history", e) - } - } - - private suspend fun fireCallbacks( - context: Context, - db: DailyNotificationDatabase, - eventType: String, - contentCache: ContentCache - ) { - try { - val callbacks = db.callbackDao().getEnabled() - callbacks.forEach { callback -> - try { - when (callback.kind) { - "http" -> fireHttpCallback(callback, eventType, contentCache) - "local" -> fireLocalCallback(context, callback, eventType, contentCache) - else -> Log.w(TAG, "Unknown callback kind: ${callback.kind}") - } - } catch (e: Exception) { - Log.e(TAG, "Failed to fire callback ${callback.id}", e) - recordHistory(db, "callback", "failure", "{\"callback_id\": \"${callback.id}\", \"error\": \"${e.message}\"}") - } - } - } catch (e: Exception) { - Log.e(TAG, "Failed to fire callbacks", e) - } - } - - private suspend fun fireHttpCallback( - callback: Callback, - eventType: String, - contentCache: ContentCache - ) { - // HTTP callback implementation would go here - Log.i(TAG, "HTTP callback fired: ${callback.id} for event: $eventType") - } - - private suspend fun fireLocalCallback( - context: Context, - callback: Callback, - eventType: String, - contentCache: ContentCache - ) { - // Local callback implementation would go here - Log.i(TAG, "Local callback fired: ${callback.id} for event: $eventType") - } - - // Static Reminder Helper Methods - private fun showStaticReminderNotification( - context: Context, - title: String, - body: String, - sound: Boolean, - vibration: Boolean, - priority: String, - reminderId: String - ) { - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - // Create notification channel for reminders - createReminderNotificationChannel(context, notificationManager) - - val notification = NotificationCompat.Builder(context, "daily_reminders") - .setSmallIcon(android.R.drawable.ic_dialog_info) - .setContentTitle(title) - .setContentText(body) - .setPriority( - when (priority) { - "high" -> NotificationCompat.PRIORITY_HIGH - "low" -> NotificationCompat.PRIORITY_LOW - else -> NotificationCompat.PRIORITY_DEFAULT - } - ) - .setSound(if (sound) null else null) // Use default sound if enabled - .setAutoCancel(true) - .setVibrate(if (vibration) longArrayOf(0, 250, 250, 250) else null) - .setCategory(NotificationCompat.CATEGORY_REMINDER) - .build() - - notificationManager.notify(reminderId.hashCode(), notification) - Log.i(TAG, "Static reminder displayed: $title (ID: $reminderId)") - } - - private fun createReminderNotificationChannel(context: Context, notificationManager: NotificationManager) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - "daily_reminders", - "Daily Reminders", - NotificationManager.IMPORTANCE_DEFAULT - ).apply { - description = "Daily reminder notifications" - enableVibration(true) - setShowBadge(true) - } - notificationManager.createNotificationChannel(channel) - } - } - - private fun recordReminderTrigger(context: Context, reminderId: String) { - try { - val prefs = context.getSharedPreferences("daily_reminders", Context.MODE_PRIVATE) - val editor = prefs.edit() - editor.putLong("${reminderId}_lastTriggered", System.currentTimeMillis()) - editor.apply() - Log.d(TAG, "Reminder trigger recorded: $reminderId") - } catch (e: Exception) { - Log.e(TAG, "Error recording reminder trigger", e) - } - } -} diff --git a/android/build.gradle b/android/build.gradle index 670d4e0..cc6d3db 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,29 +1,102 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. +apply plugin: 'com.android.library' buildscript { - repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.13.0' - classpath 'com.google.gms:google-services:4.4.0' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files } } -apply from: "variables.gradle" +android { + namespace "com.timesafari.dailynotification.plugin" + compileSdk 35 + + defaultConfig { + minSdk 23 + targetSdk 35 -allprojects { - repositories { - google() - mavenCentral() + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + // Disable test compilation - tests reference deprecated/removed code + // TODO: Rewrite tests to use modern AndroidX testing framework + testOptions { + unitTests.all { + enabled = false + } + } + + // Exclude test sources from compilation + sourceSets { + test { + java { + srcDirs = [] // Disable test source compilation + } + } } } -task clean(type: Delete) { - delete rootProject.buildDir +repositories { + google() + mavenCentral() + + // Try to find Capacitor from node_modules (for standalone builds) + // In consuming apps, Capacitor will be available as a project dependency + def capacitorPath = new File(rootProject.projectDir, '../node_modules/@capacitor/android/capacitor') + if (capacitorPath.exists()) { + flatDir { + dirs capacitorPath + } + } } + +dependencies { + // Capacitor dependency - provided by consuming app + // When included as a project dependency, use project reference + // When building standalone, this will fail (expected - plugin must be built within a Capacitor app) + def capacitorProject = project.findProject(':capacitor-android') + if (capacitorProject != null) { + implementation capacitorProject + } else { + // Try to find from node_modules (for syntax checking only) + def capacitorPath = new File(rootProject.projectDir, '../node_modules/@capacitor/android/capacitor') + if (capacitorPath.exists() && new File(capacitorPath, 'build.gradle').exists()) { + // If we're in a Capacitor app context, try to include it + throw new GradleException("Capacitor Android project not found. This plugin must be built within a Capacitor app that includes :capacitor-android.") + } else { + throw new GradleException("Capacitor Android not found. This plugin must be built within a Capacitor app context.") + } + } + + // These dependencies are always available from Maven + implementation "androidx.appcompat:appcompat:1.7.0" + 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" + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.10" + implementation "com.google.code.gson:gson:2.10.1" + implementation "androidx.core:core:1.12.0" + + annotationProcessor "androidx.room:room-compiler:2.6.1" +} + diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle deleted file mode 100644 index 9a5fa87..0000000 --- a/android/capacitor.settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN -include ':capacitor-android' -project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') diff --git a/android/consumer-rules.pro b/android/consumer-rules.pro new file mode 100644 index 0000000..e802fb5 --- /dev/null +++ b/android/consumer-rules.pro @@ -0,0 +1,10 @@ +# Consumer ProGuard rules for Daily Notification Plugin +# These rules are applied to consuming apps when they use this plugin + +# Keep plugin classes +-keep class com.timesafari.dailynotification.** { *; } + +# Keep Capacitor plugin interface +-keep class com.getcapacitor.Plugin { *; } +-keep @com.getcapacitor.Plugin class * { *; } + diff --git a/android/gradle.properties b/android/gradle.properties index 2e87c52..3a1b633 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,22 +1,29 @@ -# Project-wide Gradle settings. +# Project-wide Gradle settings for Daily Notification Plugin -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. +# AndroidX package structure to make it clearer which packages are bundled with the +# AndroidX library +android.useAndroidX=true -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true + +# Enable Gradle build cache +org.gradle.caching=true + +# Enable parallel builds +org.gradle.parallel=true + +# Increase memory for Gradle daemon +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# Enable configuration cache +org.gradle.configuration-cache=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar index d64cd49..a4b76b9 100644 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradlew b/android/gradlew index 1aa94a4..f5feea6 100755 --- a/android/gradlew +++ b/android/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# 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/. @@ -84,7 +86,8 @@ done # 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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +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 diff --git a/android/gradlew.bat b/android/gradlew.bat index 25da30d..9d21a21 100644 --- a/android/gradlew.bat +++ b/android/gradlew.bat @@ -13,6 +13,8 @@ @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 ########################################################################## diff --git a/android/plugin/build.gradle b/android/plugin/build.gradle deleted file mode 100644 index 30aaa0a..0000000 --- a/android/plugin/build.gradle +++ /dev/null @@ -1,67 +0,0 @@ -apply plugin: 'com.android.library' - -android { - namespace "com.timesafari.dailynotification.plugin" - compileSdkVersion rootProject.ext.compileSdkVersion - - defaultConfig { - minSdkVersion rootProject.ext.minSdkVersion - targetSdkVersion rootProject.ext.targetSdkVersion - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - debug { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - // Disable test compilation - tests reference deprecated/removed code - // TODO: Rewrite tests to use modern AndroidX testing framework - testOptions { - unitTests.all { - enabled = false - } - } - - // Exclude test sources from compilation - sourceSets { - test { - java { - srcDirs = [] // Disable test source compilation - } - } - } -} - -dependencies { - implementation project(':capacitor-android') - implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" - 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" - implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.10" - implementation "com.google.code.gson:gson:2.10.1" - implementation "androidx.core:core:1.12.0" - - annotationProcessor "androidx.room:room-compiler:2.6.1" - annotationProcessor project(':capacitor-android') - - // Temporarily disabled tests due to deprecated Android testing APIs - // TODO: Update test files to use modern AndroidX testing framework - // testImplementation "junit:junit:$junitVersion" - // androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" - // androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" -} diff --git a/android/plugin/src/test/java/com/timesafari/dailynotification/DailyNotificationDatabaseTest.java b/android/plugin/src/test/java/com/timesafari/dailynotification/DailyNotificationDatabaseTest.java deleted file mode 100644 index 811b93d..0000000 --- a/android/plugin/src/test/java/com/timesafari/dailynotification/DailyNotificationDatabaseTest.java +++ /dev/null @@ -1,215 +0,0 @@ -/** - * DailyNotificationDatabaseTest.java - * - * Unit tests for SQLite database functionality - * Tests schema creation, WAL mode, and basic operations - * - * @author Matthew Raymer - * @version 1.0.0 - */ - -package com.timesafari.dailynotification; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.test.AndroidTestCase; -import android.test.mock.MockContext; - -import java.io.File; - -/** - * Unit tests for DailyNotificationDatabase - * - * Tests the core SQLite functionality including: - * - Database creation and schema - * - WAL mode configuration - * - Table and index creation - * - Schema version management - */ -public class DailyNotificationDatabaseTest extends AndroidTestCase { - - private DailyNotificationDatabase database; - private Context mockContext; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - // Create mock context - mockContext = new MockContext() { - @Override - public File getDatabasePath(String name) { - return new File(getContext().getCacheDir(), name); - } - }; - - // Create database instance - database = new DailyNotificationDatabase(mockContext); - } - - @Override - protected void tearDown() throws Exception { - if (database != null) { - database.close(); - } - super.tearDown(); - } - - /** - * Test database creation and schema - */ - public void testDatabaseCreation() { - assertNotNull("Database should not be null", database); - - SQLiteDatabase db = database.getReadableDatabase(); - assertNotNull("Readable database should not be null", db); - assertTrue("Database should be open", db.isOpen()); - - db.close(); - } - - /** - * Test WAL mode configuration - */ - public void testWALModeConfiguration() { - SQLiteDatabase db = database.getWritableDatabase(); - - // Check journal mode - android.database.Cursor cursor = db.rawQuery("PRAGMA journal_mode", null); - assertTrue("Should have journal mode result", cursor.moveToFirst()); - String journalMode = cursor.getString(0); - assertEquals("Journal mode should be WAL", "wal", journalMode.toLowerCase()); - cursor.close(); - - // Check synchronous mode - cursor = db.rawQuery("PRAGMA synchronous", null); - assertTrue("Should have synchronous result", cursor.moveToFirst()); - int synchronous = cursor.getInt(0); - assertEquals("Synchronous mode should be NORMAL", 1, synchronous); - cursor.close(); - - // Check foreign keys - cursor = db.rawQuery("PRAGMA foreign_keys", null); - assertTrue("Should have foreign_keys result", cursor.moveToFirst()); - int foreignKeys = cursor.getInt(0); - assertEquals("Foreign keys should be enabled", 1, foreignKeys); - cursor.close(); - - db.close(); - } - - /** - * Test table creation - */ - public void testTableCreation() { - SQLiteDatabase db = database.getWritableDatabase(); - - // Check if tables exist - assertTrue("notif_contents table should exist", - tableExists(db, DailyNotificationDatabase.TABLE_NOTIF_CONTENTS)); - assertTrue("notif_deliveries table should exist", - tableExists(db, DailyNotificationDatabase.TABLE_NOTIF_DELIVERIES)); - assertTrue("notif_config table should exist", - tableExists(db, DailyNotificationDatabase.TABLE_NOTIF_CONFIG)); - - db.close(); - } - - /** - * Test index creation - */ - public void testIndexCreation() { - SQLiteDatabase db = database.getWritableDatabase(); - - // Check if indexes exist - assertTrue("notif_idx_contents_slot_time index should exist", - indexExists(db, "notif_idx_contents_slot_time")); - assertTrue("notif_idx_deliveries_slot index should exist", - indexExists(db, "notif_idx_deliveries_slot")); - - db.close(); - } - - /** - * Test schema version management - */ - public void testSchemaVersion() { - SQLiteDatabase db = database.getWritableDatabase(); - - // Check user_version - android.database.Cursor cursor = db.rawQuery("PRAGMA user_version", null); - assertTrue("Should have user_version result", cursor.moveToFirst()); - int userVersion = cursor.getInt(0); - assertEquals("User version should match database version", - DailyNotificationDatabase.DATABASE_VERSION, userVersion); - cursor.close(); - - db.close(); - } - - /** - * Test basic insert operations - */ - public void testBasicInsertOperations() { - SQLiteDatabase db = database.getWritableDatabase(); - - // Test inserting into notif_contents - android.content.ContentValues values = new android.content.ContentValues(); - values.put(DailyNotificationDatabase.COL_CONTENTS_SLOT_ID, "test_slot_1"); - values.put(DailyNotificationDatabase.COL_CONTENTS_PAYLOAD_JSON, "{\"title\":\"Test\"}"); - values.put(DailyNotificationDatabase.COL_CONTENTS_FETCHED_AT, System.currentTimeMillis()); - - long rowId = db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, null, values); - assertTrue("Insert should succeed", rowId > 0); - - // Test inserting into notif_config - values.clear(); - values.put(DailyNotificationDatabase.COL_CONFIG_K, "test_key"); - values.put(DailyNotificationDatabase.COL_CONFIG_V, "test_value"); - - rowId = db.insert(DailyNotificationDatabase.TABLE_NOTIF_CONFIG, null, values); - assertTrue("Config insert should succeed", rowId > 0); - - db.close(); - } - - /** - * Test database file operations - */ - public void testDatabaseFileOperations() { - String dbPath = database.getDatabasePath(); - assertNotNull("Database path should not be null", dbPath); - assertTrue("Database path should not be empty", !dbPath.isEmpty()); - - // Database should exist after creation - assertTrue("Database file should exist", database.databaseExists()); - - // Database size should be greater than 0 - long size = database.getDatabaseSize(); - assertTrue("Database size should be greater than 0", size > 0); - } - - /** - * Helper method to check if table exists - */ - private boolean tableExists(SQLiteDatabase db, String tableName) { - android.database.Cursor cursor = db.rawQuery( - "SELECT name FROM sqlite_master WHERE type='table' AND name=?", - new String[]{tableName}); - boolean exists = cursor.moveToFirst(); - cursor.close(); - return exists; - } - - /** - * Helper method to check if index exists - */ - private boolean indexExists(SQLiteDatabase db, String indexName) { - android.database.Cursor cursor = db.rawQuery( - "SELECT name FROM sqlite_master WHERE type='index' AND name=?", - new String[]{indexName}); - boolean exists = cursor.moveToFirst(); - cursor.close(); - return exists; - } -} diff --git a/android/plugin/src/test/java/com/timesafari/dailynotification/DailyNotificationRollingWindowTest.java b/android/plugin/src/test/java/com/timesafari/dailynotification/DailyNotificationRollingWindowTest.java deleted file mode 100644 index 40d5929..0000000 --- a/android/plugin/src/test/java/com/timesafari/dailynotification/DailyNotificationRollingWindowTest.java +++ /dev/null @@ -1,193 +0,0 @@ -/** - * DailyNotificationRollingWindowTest.java - * - * Unit tests for rolling window safety functionality - * Tests window maintenance, capacity management, and platform-specific limits - * - * @author Matthew Raymer - * @version 1.0.0 - */ - -package com.timesafari.dailynotification; - -import android.content.Context; -import android.test.AndroidTestCase; -import android.test.mock.MockContext; - -import java.util.concurrent.TimeUnit; - -/** - * Unit tests for DailyNotificationRollingWindow - * - * Tests the rolling window safety functionality including: - * - Window maintenance and state updates - * - Capacity limit enforcement - * - Platform-specific behavior (iOS vs Android) - * - Statistics and maintenance timing - */ -public class DailyNotificationRollingWindowTest extends AndroidTestCase { - - private DailyNotificationRollingWindow rollingWindow; - private Context mockContext; - private DailyNotificationScheduler mockScheduler; - private DailyNotificationTTLEnforcer mockTTLEnforcer; - private DailyNotificationStorage mockStorage; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - // Create mock context - mockContext = new MockContext() { - @Override - public android.content.SharedPreferences getSharedPreferences(String name, int mode) { - return getContext().getSharedPreferences(name, mode); - } - }; - - // Create mock components - mockScheduler = new MockDailyNotificationScheduler(); - mockTTLEnforcer = new MockDailyNotificationTTLEnforcer(); - mockStorage = new MockDailyNotificationStorage(); - - // Create rolling window for Android platform - rollingWindow = new DailyNotificationRollingWindow( - mockContext, - mockScheduler, - mockTTLEnforcer, - mockStorage, - false // Android platform - ); - } - - @Override - protected void tearDown() throws Exception { - super.tearDown(); - } - - /** - * Test rolling window initialization - */ - public void testRollingWindowInitialization() { - assertNotNull("Rolling window should be initialized", rollingWindow); - - // Test Android platform limits - String stats = rollingWindow.getRollingWindowStats(); - assertNotNull("Stats should not be null", stats); - assertTrue("Stats should contain Android platform info", stats.contains("Android")); - } - - /** - * Test rolling window maintenance - */ - public void testRollingWindowMaintenance() { - // Test that maintenance can be forced - rollingWindow.forceMaintenance(); - - // Test maintenance timing - assertFalse("Maintenance should not be needed immediately after forcing", - rollingWindow.isMaintenanceNeeded()); - - // Test time until next maintenance - long timeUntilNext = rollingWindow.getTimeUntilNextMaintenance(); - assertTrue("Time until next maintenance should be positive", timeUntilNext > 0); - } - - /** - * Test iOS platform behavior - */ - public void testIOSPlatformBehavior() { - // Create rolling window for iOS platform - DailyNotificationRollingWindow iosRollingWindow = new DailyNotificationRollingWindow( - mockContext, - mockScheduler, - mockTTLEnforcer, - mockStorage, - true // iOS platform - ); - - String stats = iosRollingWindow.getRollingWindowStats(); - assertNotNull("iOS stats should not be null", stats); - assertTrue("Stats should contain iOS platform info", stats.contains("iOS")); - } - - /** - * Test maintenance timing - */ - public void testMaintenanceTiming() { - // Initially, maintenance should not be needed - assertFalse("Maintenance should not be needed initially", - rollingWindow.isMaintenanceNeeded()); - - // Force maintenance - rollingWindow.forceMaintenance(); - - // Should not be needed immediately after - assertFalse("Maintenance should not be needed after forcing", - rollingWindow.isMaintenanceNeeded()); - } - - /** - * Test statistics retrieval - */ - public void testStatisticsRetrieval() { - String stats = rollingWindow.getRollingWindowStats(); - - assertNotNull("Statistics should not be null", stats); - assertTrue("Statistics should contain pending count", stats.contains("pending")); - assertTrue("Statistics should contain daily count", stats.contains("daily")); - assertTrue("Statistics should contain platform info", stats.contains("platform")); - } - - /** - * Test error handling - */ - public void testErrorHandling() { - // Test with null components (should not crash) - try { - DailyNotificationRollingWindow errorWindow = new DailyNotificationRollingWindow( - null, null, null, null, false - ); - // Should not crash during construction - } catch (Exception e) { - // Expected to handle gracefully - } - } - - /** - * Mock DailyNotificationScheduler for testing - */ - private static class MockDailyNotificationScheduler extends DailyNotificationScheduler { - public MockDailyNotificationScheduler() { - super(null, null); - } - - @Override - public boolean scheduleNotification(NotificationContent content) { - return true; // Always succeed for testing - } - } - - /** - * Mock DailyNotificationTTLEnforcer for testing - */ - private static class MockDailyNotificationTTLEnforcer extends DailyNotificationTTLEnforcer { - public MockDailyNotificationTTLEnforcer() { - super(null, null, false); - } - - @Override - public boolean validateBeforeArming(NotificationContent content) { - return true; // Always pass validation for testing - } - } - - /** - * Mock DailyNotificationStorage for testing - */ - private static class MockDailyNotificationStorage extends DailyNotificationStorage { - public MockDailyNotificationStorage() { - super(null); - } - } -} diff --git a/android/plugin/src/test/java/com/timesafari/dailynotification/DailyNotificationTTLEnforcerTest.java b/android/plugin/src/test/java/com/timesafari/dailynotification/DailyNotificationTTLEnforcerTest.java deleted file mode 100644 index e932331..0000000 --- a/android/plugin/src/test/java/com/timesafari/dailynotification/DailyNotificationTTLEnforcerTest.java +++ /dev/null @@ -1,217 +0,0 @@ -/** - * DailyNotificationTTLEnforcerTest.java - * - * Unit tests for TTL-at-fire enforcement functionality - * Tests freshness validation, TTL violation logging, and skip logic - * - * @author Matthew Raymer - * @version 1.0.0 - */ - -package com.timesafari.dailynotification; - -import android.content.Context; -import android.test.AndroidTestCase; -import android.test.mock.MockContext; - -import java.util.concurrent.TimeUnit; - -/** - * Unit tests for DailyNotificationTTLEnforcer - * - * Tests the core TTL enforcement functionality including: - * - Freshness validation before arming - * - TTL violation detection and logging - * - Skip logic for stale content - * - Configuration retrieval from storage - */ -public class DailyNotificationTTLEnforcerTest extends AndroidTestCase { - - private DailyNotificationTTLEnforcer ttlEnforcer; - private Context mockContext; - private DailyNotificationDatabase database; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - // Create mock context - mockContext = new MockContext() { - @Override - public android.content.SharedPreferences getSharedPreferences(String name, int mode) { - return getContext().getSharedPreferences(name, mode); - } - }; - - // Create database instance - database = new DailyNotificationDatabase(mockContext); - - // Create TTL enforcer with SQLite storage - ttlEnforcer = new DailyNotificationTTLEnforcer(mockContext, database, true); - } - - @Override - protected void tearDown() throws Exception { - if (database != null) { - database.close(); - } - super.tearDown(); - } - - /** - * Test freshness validation with fresh content - */ - public void testFreshContentValidation() { - long currentTime = System.currentTimeMillis(); - long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); // 30 minutes from now - long fetchedAt = currentTime - TimeUnit.MINUTES.toMillis(5); // 5 minutes ago - - boolean isFresh = ttlEnforcer.isContentFresh("test_slot_1", scheduledTime, fetchedAt); - - assertTrue("Content should be fresh (5 min old, scheduled 30 min from now)", isFresh); - } - - /** - * Test freshness validation with stale content - */ - public void testStaleContentValidation() { - long currentTime = System.currentTimeMillis(); - long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); // 30 minutes from now - long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(2); // 2 hours ago - - boolean isFresh = ttlEnforcer.isContentFresh("test_slot_2", scheduledTime, fetchedAt); - - assertFalse("Content should be stale (2 hours old, exceeds 1 hour TTL)", isFresh); - } - - /** - * Test TTL violation detection - */ - public void testTTLViolationDetection() { - long currentTime = System.currentTimeMillis(); - long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); - long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(2); // 2 hours ago - - // This should trigger a TTL violation - boolean isFresh = ttlEnforcer.isContentFresh("test_slot_3", scheduledTime, fetchedAt); - - assertFalse("Should detect TTL violation", isFresh); - - // Check that violation was logged (we can't easily test the actual logging, - // but we can verify the method returns false as expected) - } - - /** - * Test validateBeforeArming with fresh content - */ - public void testValidateBeforeArmingFresh() { - long currentTime = System.currentTimeMillis(); - long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); - long fetchedAt = currentTime - TimeUnit.MINUTES.toMillis(5); - - NotificationContent content = new NotificationContent(); - content.setId("test_slot_4"); - content.setScheduledTime(scheduledTime); - content.setFetchedAt(fetchedAt); - content.setTitle("Test Notification"); - content.setBody("Test body"); - - boolean shouldArm = ttlEnforcer.validateBeforeArming(content); - - assertTrue("Should arm fresh content", shouldArm); - } - - /** - * Test validateBeforeArming with stale content - */ - public void testValidateBeforeArmingStale() { - long currentTime = System.currentTimeMillis(); - long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); - long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(2); - - NotificationContent content = new NotificationContent(); - content.setId("test_slot_5"); - content.setScheduledTime(scheduledTime); - content.setFetchedAt(fetchedAt); - content.setTitle("Test Notification"); - content.setBody("Test body"); - - boolean shouldArm = ttlEnforcer.validateBeforeArming(content); - - assertFalse("Should not arm stale content", shouldArm); - } - - /** - * Test edge case: content fetched exactly at TTL limit - */ - public void testTTLBoundaryCase() { - long currentTime = System.currentTimeMillis(); - long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); - long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(1); // Exactly 1 hour ago (TTL limit) - - boolean isFresh = ttlEnforcer.isContentFresh("test_slot_6", scheduledTime, fetchedAt); - - assertTrue("Content at TTL boundary should be considered fresh", isFresh); - } - - /** - * Test edge case: content fetched just over TTL limit - */ - public void testTTLBoundaryCaseOver() { - long currentTime = System.currentTimeMillis(); - long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); - long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(1) - TimeUnit.SECONDS.toMillis(1); // 1 hour + 1 second ago - - boolean isFresh = ttlEnforcer.isContentFresh("test_slot_7", scheduledTime, fetchedAt); - - assertFalse("Content just over TTL limit should be considered stale", isFresh); - } - - /** - * Test TTL violation statistics - */ - public void testTTLViolationStats() { - // Generate some TTL violations - long currentTime = System.currentTimeMillis(); - long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); - long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(2); - - // Trigger TTL violations - ttlEnforcer.isContentFresh("test_slot_8", scheduledTime, fetchedAt); - ttlEnforcer.isContentFresh("test_slot_9", scheduledTime, fetchedAt); - - String stats = ttlEnforcer.getTTLViolationStats(); - - assertNotNull("TTL violation stats should not be null", stats); - assertTrue("Stats should contain violation count", stats.contains("violations")); - } - - /** - * Test error handling with invalid parameters - */ - public void testErrorHandling() { - // Test with null slot ID - boolean result = ttlEnforcer.isContentFresh(null, System.currentTimeMillis(), System.currentTimeMillis()); - assertFalse("Should handle null slot ID gracefully", result); - - // Test with invalid timestamps - result = ttlEnforcer.isContentFresh("test_slot_10", 0, 0); - assertTrue("Should handle invalid timestamps gracefully", result); - } - - /** - * Test TTL configuration retrieval - */ - public void testTTLConfiguration() { - // Test that TTL enforcer can retrieve configuration - // This is indirectly tested through the freshness checks - long currentTime = System.currentTimeMillis(); - long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); - long fetchedAt = currentTime - TimeUnit.MINUTES.toMillis(30); // 30 minutes ago - - boolean isFresh = ttlEnforcer.isContentFresh("test_slot_11", scheduledTime, fetchedAt); - - // Should be fresh (30 min < 1 hour TTL) - assertTrue("Should retrieve TTL configuration correctly", isFresh); - } -} diff --git a/android/settings.gradle b/android/settings.gradle index 3787e02..3ff3467 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,6 +1,7 @@ -include ':app' -include ':plugin' -include ':capacitor-cordova-android-plugins' -project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') +// Settings file for Daily Notification Plugin +// This is a minimal settings.gradle for a Capacitor plugin module +// Capacitor plugins don't typically need a settings.gradle, but it's included +// for standalone builds and Android Studio compatibility + +rootProject.name = 'daily-notification-plugin' -apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..59e2a6f --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.java b/android/src/main/java/com/timesafari/dailynotification/BootReceiver.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.java rename to android/src/main/java/com/timesafari/dailynotification/BootReceiver.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/ChannelManager.java b/android/src/main/java/com/timesafari/dailynotification/ChannelManager.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/ChannelManager.java rename to android/src/main/java/com/timesafari/dailynotification/ChannelManager.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationETagManager.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationETagManager.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationETagManager.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationETagManager.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationErrorHandler.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationErrorHandler.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationErrorHandler.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationErrorHandler.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationExactAlarmManager.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationExactAlarmManager.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationExactAlarmManager.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationExactAlarmManager.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetcher.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetcher.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetcher.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetcher.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationJWTManager.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationJWTManager.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationJWTManager.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationJWTManager.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationMaintenanceWorker.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationMaintenanceWorker.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationMaintenanceWorker.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationMaintenanceWorker.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationMigration.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationMigration.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationMigration.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationMigration.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPerformanceOptimizer.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPerformanceOptimizer.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPerformanceOptimizer.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationPerformanceOptimizer.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationRebootRecoveryManager.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationRebootRecoveryManager.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationRebootRecoveryManager.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationRebootRecoveryManager.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationScheduler.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationStorage.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationTTLEnforcer.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationTTLEnforcer.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationTTLEnforcer.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationTTLEnforcer.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java rename to android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyReminderInfo.java b/android/src/main/java/com/timesafari/dailynotification/DailyReminderInfo.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyReminderInfo.java rename to android/src/main/java/com/timesafari/dailynotification/DailyReminderInfo.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyReminderManager.java b/android/src/main/java/com/timesafari/dailynotification/DailyReminderManager.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DailyReminderManager.java rename to android/src/main/java/com/timesafari/dailynotification/DailyReminderManager.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DozeFallbackWorker.java b/android/src/main/java/com/timesafari/dailynotification/DozeFallbackWorker.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/DozeFallbackWorker.java rename to android/src/main/java/com/timesafari/dailynotification/DozeFallbackWorker.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/EnhancedDailyNotificationFetcher.java b/android/src/main/java/com/timesafari/dailynotification/EnhancedDailyNotificationFetcher.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/EnhancedDailyNotificationFetcher.java rename to android/src/main/java/com/timesafari/dailynotification/EnhancedDailyNotificationFetcher.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/FetchContext.java b/android/src/main/java/com/timesafari/dailynotification/FetchContext.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/FetchContext.java rename to android/src/main/java/com/timesafari/dailynotification/FetchContext.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/NativeNotificationContentFetcher.java b/android/src/main/java/com/timesafari/dailynotification/NativeNotificationContentFetcher.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/NativeNotificationContentFetcher.java rename to android/src/main/java/com/timesafari/dailynotification/NativeNotificationContentFetcher.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationContent.java b/android/src/main/java/com/timesafari/dailynotification/NotificationContent.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/NotificationContent.java rename to android/src/main/java/com/timesafari/dailynotification/NotificationContent.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/NotificationStatusChecker.java b/android/src/main/java/com/timesafari/dailynotification/NotificationStatusChecker.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/NotificationStatusChecker.java rename to android/src/main/java/com/timesafari/dailynotification/NotificationStatusChecker.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/PendingIntentManager.java b/android/src/main/java/com/timesafari/dailynotification/PendingIntentManager.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/PendingIntentManager.java rename to android/src/main/java/com/timesafari/dailynotification/PendingIntentManager.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/PermissionManager.java b/android/src/main/java/com/timesafari/dailynotification/PermissionManager.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/PermissionManager.java rename to android/src/main/java/com/timesafari/dailynotification/PermissionManager.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/SchedulingPolicy.java b/android/src/main/java/com/timesafari/dailynotification/SchedulingPolicy.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/SchedulingPolicy.java rename to android/src/main/java/com/timesafari/dailynotification/SchedulingPolicy.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/SoftRefetchWorker.java b/android/src/main/java/com/timesafari/dailynotification/SoftRefetchWorker.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/SoftRefetchWorker.java rename to android/src/main/java/com/timesafari/dailynotification/SoftRefetchWorker.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java b/android/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java rename to android/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/dao/NotificationConfigDao.java b/android/src/main/java/com/timesafari/dailynotification/dao/NotificationConfigDao.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/dao/NotificationConfigDao.java rename to android/src/main/java/com/timesafari/dailynotification/dao/NotificationConfigDao.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/dao/NotificationContentDao.java b/android/src/main/java/com/timesafari/dailynotification/dao/NotificationContentDao.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/dao/NotificationContentDao.java rename to android/src/main/java/com/timesafari/dailynotification/dao/NotificationContentDao.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/dao/NotificationDeliveryDao.java b/android/src/main/java/com/timesafari/dailynotification/dao/NotificationDeliveryDao.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/dao/NotificationDeliveryDao.java rename to android/src/main/java/com/timesafari/dailynotification/dao/NotificationDeliveryDao.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/database/DailyNotificationDatabase.java b/android/src/main/java/com/timesafari/dailynotification/database/DailyNotificationDatabase.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/database/DailyNotificationDatabase.java rename to android/src/main/java/com/timesafari/dailynotification/database/DailyNotificationDatabase.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/entities/NotificationConfigEntity.java b/android/src/main/java/com/timesafari/dailynotification/entities/NotificationConfigEntity.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/entities/NotificationConfigEntity.java rename to android/src/main/java/com/timesafari/dailynotification/entities/NotificationConfigEntity.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/entities/NotificationContentEntity.java b/android/src/main/java/com/timesafari/dailynotification/entities/NotificationContentEntity.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/entities/NotificationContentEntity.java rename to android/src/main/java/com/timesafari/dailynotification/entities/NotificationContentEntity.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/entities/NotificationDeliveryEntity.java b/android/src/main/java/com/timesafari/dailynotification/entities/NotificationDeliveryEntity.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/entities/NotificationDeliveryEntity.java rename to android/src/main/java/com/timesafari/dailynotification/entities/NotificationDeliveryEntity.java diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/storage/DailyNotificationStorageRoom.java b/android/src/main/java/com/timesafari/dailynotification/storage/DailyNotificationStorageRoom.java similarity index 100% rename from android/plugin/src/main/java/com/timesafari/dailynotification/storage/DailyNotificationStorageRoom.java rename to android/src/main/java/com/timesafari/dailynotification/storage/DailyNotificationStorageRoom.java diff --git a/package.json b/package.json index bd3bd5b..98a7815 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@timesafari/daily-notification-plugin", - "version": "1.0.0", + "version": "1.0.1", "description": "TimeSafari Daily Notification Plugin - Enterprise-grade daily notification functionality with dual scheduling, callback support, TTL-at-fire logic, and comprehensive observability across Mobile (Capacitor) and Desktop (Electron) platforms", "main": "dist/plugin.js", "module": "dist/esm/index.js", diff --git a/scripts/fix-capacitor-plugin-path.js b/scripts/fix-capacitor-plugin-path.js new file mode 100755 index 0000000..6f74970 --- /dev/null +++ b/scripts/fix-capacitor-plugin-path.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +/** + * Verify Capacitor plugin Android structure (post-restructure) + * + * This script verifies that the plugin follows the standard Capacitor structure: + * - android/src/main/java/... (plugin code) + * - android/build.gradle (plugin build config) + * + * This script is now optional since the plugin uses standard structure. + * It can be used to verify the structure is correct. + * + * @author Matthew Raymer + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function findAppRoot() { + let currentDir = __dirname; + + // Go up from scripts/ to plugin root + currentDir = path.dirname(currentDir); + + // Verify we're in the plugin root + const pluginPackageJson = path.join(currentDir, 'package.json'); + if (!fs.existsSync(pluginPackageJson)) { + throw new Error('Could not find plugin package.json - script may be in wrong location'); + } + + // Go up from plugin root to node_modules/@timesafari + currentDir = path.dirname(currentDir); + + // Go up from node_modules/@timesafari to node_modules + currentDir = path.dirname(currentDir); + + // Go up from node_modules to app root + const appRoot = path.dirname(currentDir); + + // Verify we found an app root + const androidDir = path.join(appRoot, 'android'); + if (!fs.existsSync(androidDir)) { + throw new Error(`Could not find app android directory. Looked in: ${appRoot}`); + } + + return appRoot; +} + +/** + * Verify plugin uses standard Capacitor structure + */ +function verifyPluginStructure() { + console.log('🔍 Verifying Daily Notification Plugin structure...'); + + try { + const APP_ROOT = findAppRoot(); + const PLUGIN_PATH = path.join(APP_ROOT, 'node_modules', '@timesafari', 'daily-notification-plugin'); + const ANDROID_PLUGIN_PATH = path.join(PLUGIN_PATH, 'android'); + const PLUGIN_JAVA_PATH = path.join(ANDROID_PLUGIN_PATH, 'src', 'main', 'java'); + + if (!fs.existsSync(ANDROID_PLUGIN_PATH)) { + console.log('ℹ️ Plugin not found in node_modules (may not be installed yet)'); + return; + } + + // Check for standard structure + const hasStandardStructure = fs.existsSync(PLUGIN_JAVA_PATH); + const hasOldStructure = fs.existsSync(path.join(ANDROID_PLUGIN_PATH, 'plugin')); + + if (hasOldStructure) { + console.log('⚠️ WARNING: Plugin still uses old structure (android/plugin/)'); + console.log(' This should not happen after restructure. Please rebuild plugin.'); + return; + } + + if (hasStandardStructure) { + console.log('✅ Plugin uses standard Capacitor structure (android/src/main/java/)'); + console.log(' No fixes needed - plugin path is correct!'); + } else { + console.log('⚠️ Plugin structure not recognized'); + console.log(` Expected: ${PLUGIN_JAVA_PATH}`); + } + + } catch (error) { + console.error('❌ Error verifying plugin structure:', error.message); + process.exit(1); + } +} + +/** + * Run verification + */ +function verifyAll() { + console.log('🔍 Daily Notification Plugin - Structure Verification'); + console.log('==================================================\n'); + + verifyPluginStructure(); + + console.log('\n✅ Verification complete!'); +} + +// Run if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + verifyAll(); +} + +export { verifyPluginStructure, verifyAll }; diff --git a/test-apps/BUILD_PROCESS.md b/test-apps/BUILD_PROCESS.md new file mode 100644 index 0000000..602d61d --- /dev/null +++ b/test-apps/BUILD_PROCESS.md @@ -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! diff --git a/android/app/.gitignore b/test-apps/android-test-app/app/.gitignore similarity index 100% rename from android/app/.gitignore rename to test-apps/android-test-app/app/.gitignore diff --git a/android/app/build.gradle b/test-apps/android-test-app/app/build.gradle similarity index 92% rename from android/app/build.gradle rename to test-apps/android-test-app/app/build.gradle index 96da676..9cb7f2c 100644 --- a/android/app/build.gradle +++ b/test-apps/android-test-app/app/build.gradle @@ -26,7 +26,7 @@ android { repositories { flatDir{ - dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + dirs 'libs' } } @@ -36,7 +36,7 @@ dependencies { implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation project(':capacitor-android') - implementation project(':plugin') + implementation project(':daily-notification-plugin') // Daily Notification Plugin Dependencies implementation "androidx.room:room-runtime:2.6.1" @@ -47,7 +47,7 @@ dependencies { testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" - implementation project(':capacitor-cordova-android-plugins') + // Note: capacitor-cordova-android-plugins not needed for standalone Android test app } apply from: 'capacitor.build.gradle' diff --git a/android/app/capacitor.build.gradle b/test-apps/android-test-app/app/capacitor.build.gradle similarity index 100% rename from android/app/capacitor.build.gradle rename to test-apps/android-test-app/app/capacitor.build.gradle diff --git a/android/app/proguard-rules.pro b/test-apps/android-test-app/app/proguard-rules.pro similarity index 100% rename from android/app/proguard-rules.pro rename to test-apps/android-test-app/app/proguard-rules.pro diff --git a/android/app/src/androidTest/java/com/timesafari/dailynotification/ExampleInstrumentedTest.java b/test-apps/android-test-app/app/src/androidTest/java/com/timesafari/dailynotification/ExampleInstrumentedTest.java similarity index 100% rename from android/app/src/androidTest/java/com/timesafari/dailynotification/ExampleInstrumentedTest.java rename to test-apps/android-test-app/app/src/androidTest/java/com/timesafari/dailynotification/ExampleInstrumentedTest.java diff --git a/android/app/src/main/AndroidManifest.xml b/test-apps/android-test-app/app/src/main/AndroidManifest.xml similarity index 100% rename from android/app/src/main/AndroidManifest.xml rename to test-apps/android-test-app/app/src/main/AndroidManifest.xml diff --git a/test-apps/android-test-app/app/src/main/assets/capacitor.config.json b/test-apps/android-test-app/app/src/main/assets/capacitor.config.json new file mode 100644 index 0000000..012f498 --- /dev/null +++ b/test-apps/android-test-app/app/src/main/assets/capacitor.config.json @@ -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 + } + } +} diff --git a/android/app/src/main/assets/capacitor.plugins.json b/test-apps/android-test-app/app/src/main/assets/capacitor.plugins.json similarity index 100% rename from android/app/src/main/assets/capacitor.plugins.json rename to test-apps/android-test-app/app/src/main/assets/capacitor.plugins.json diff --git a/android/Configure b/test-apps/android-test-app/app/src/main/assets/public/cordova.js similarity index 100% rename from android/Configure rename to test-apps/android-test-app/app/src/main/assets/public/cordova.js diff --git a/test-apps/android-test-app/app/src/main/assets/public/cordova_plugins.js b/test-apps/android-test-app/app/src/main/assets/public/cordova_plugins.js new file mode 100644 index 0000000..e69de29 diff --git a/test-apps/android-test-app/app/src/main/assets/public/index.html b/test-apps/android-test-app/app/src/main/assets/public/index.html new file mode 100644 index 0000000..c788751 --- /dev/null +++ b/test-apps/android-test-app/app/src/main/assets/public/index.html @@ -0,0 +1,575 @@ + + + + + + + + + DailyNotification Plugin Test + + + +
+

🔔 DailyNotification Plugin Test

+

Test the DailyNotification plugin functionality

+

Build: 2025-10-14 05:00:00 UTC

+ + + + + +

🔔 Notification Tests

+ + + + +

🔐 Permission Management

+ + + + +

📢 Channel Management

+ + + + +
+ Ready to test... +
+
+ + + + diff --git a/test-apps/android-test-app/app/src/main/assets/public/plugins b/test-apps/android-test-app/app/src/main/assets/public/plugins new file mode 100644 index 0000000..9c4fcb6 --- /dev/null +++ b/test-apps/android-test-app/app/src/main/assets/public/plugins @@ -0,0 +1,6 @@ +[ + { + "name": "DailyNotification", + "class": "com.timesafari.dailynotification.DailyNotificationPlugin" + } +] diff --git a/android/app/src/main/java/com/timesafari/dailynotification/DemoNativeFetcher.java b/test-apps/android-test-app/app/src/main/java/com/timesafari/dailynotification/DemoNativeFetcher.java similarity index 100% rename from android/app/src/main/java/com/timesafari/dailynotification/DemoNativeFetcher.java rename to test-apps/android-test-app/app/src/main/java/com/timesafari/dailynotification/DemoNativeFetcher.java diff --git a/android/app/src/main/java/com/timesafari/dailynotification/MainActivity.java b/test-apps/android-test-app/app/src/main/java/com/timesafari/dailynotification/MainActivity.java similarity index 100% rename from android/app/src/main/java/com/timesafari/dailynotification/MainActivity.java rename to test-apps/android-test-app/app/src/main/java/com/timesafari/dailynotification/MainActivity.java diff --git a/android/app/src/main/java/com/timesafari/dailynotification/PluginApplication.java b/test-apps/android-test-app/app/src/main/java/com/timesafari/dailynotification/PluginApplication.java similarity index 100% rename from android/app/src/main/java/com/timesafari/dailynotification/PluginApplication.java rename to test-apps/android-test-app/app/src/main/java/com/timesafari/dailynotification/PluginApplication.java diff --git a/android/app/src/main/res/drawable-land-hdpi/splash.png b/test-apps/android-test-app/app/src/main/res/drawable-land-hdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-land-hdpi/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable-land-hdpi/splash.png diff --git a/android/app/src/main/res/drawable-land-mdpi/splash.png b/test-apps/android-test-app/app/src/main/res/drawable-land-mdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-land-mdpi/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable-land-mdpi/splash.png diff --git a/android/app/src/main/res/drawable-land-xhdpi/splash.png b/test-apps/android-test-app/app/src/main/res/drawable-land-xhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-land-xhdpi/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable-land-xhdpi/splash.png diff --git a/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/test-apps/android-test-app/app/src/main/res/drawable-land-xxhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-land-xxhdpi/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable-land-xxhdpi/splash.png diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/test-apps/android-test-app/app/src/main/res/drawable-land-xxxhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-land-xxxhdpi/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable-land-xxxhdpi/splash.png diff --git a/android/app/src/main/res/drawable-port-hdpi/splash.png b/test-apps/android-test-app/app/src/main/res/drawable-port-hdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-port-hdpi/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable-port-hdpi/splash.png diff --git a/android/app/src/main/res/drawable-port-mdpi/splash.png b/test-apps/android-test-app/app/src/main/res/drawable-port-mdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-port-mdpi/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable-port-mdpi/splash.png diff --git a/android/app/src/main/res/drawable-port-xhdpi/splash.png b/test-apps/android-test-app/app/src/main/res/drawable-port-xhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-port-xhdpi/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable-port-xhdpi/splash.png diff --git a/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/test-apps/android-test-app/app/src/main/res/drawable-port-xxhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-port-xxhdpi/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable-port-xxhdpi/splash.png diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/test-apps/android-test-app/app/src/main/res/drawable-port-xxxhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-port-xxxhdpi/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable-port-xxxhdpi/splash.png diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/test-apps/android-test-app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to test-apps/android-test-app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/test-apps/android-test-app/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_launcher_background.xml rename to test-apps/android-test-app/app/src/main/res/drawable/ic_launcher_background.xml diff --git a/android/app/src/main/res/drawable/splash.png b/test-apps/android-test-app/app/src/main/res/drawable/splash.png similarity index 100% rename from android/app/src/main/res/drawable/splash.png rename to test-apps/android-test-app/app/src/main/res/drawable/splash.png diff --git a/android/app/src/main/res/layout/activity_main.xml b/test-apps/android-test-app/app/src/main/res/layout/activity_main.xml similarity index 100% rename from android/app/src/main/res/layout/activity_main.xml rename to test-apps/android-test-app/app/src/main/res/layout/activity_main.xml diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/test-apps/android-test-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to test-apps/android-test-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/test-apps/android-test-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to test-apps/android-test-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/test-apps/android-test-app/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to test-apps/android-test-app/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/test-apps/android-test-app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png rename to test-apps/android-test-app/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/test-apps/android-test-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to test-apps/android-test-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/test-apps/android-test-app/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to test-apps/android-test-app/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/test-apps/android-test-app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png rename to test-apps/android-test-app/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/test-apps/android-test-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to test-apps/android-test-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/test-apps/android-test-app/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to test-apps/android-test-app/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/test-apps/android-test-app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png rename to test-apps/android-test-app/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/test-apps/android-test-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to test-apps/android-test-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/test-apps/android-test-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to test-apps/android-test-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/test-apps/android-test-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png rename to test-apps/android-test-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/test-apps/android-test-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to test-apps/android-test-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/test-apps/android-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to test-apps/android-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/test-apps/android-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png rename to test-apps/android-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/test-apps/android-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to test-apps/android-test-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/test-apps/android-test-app/app/src/main/res/values/ic_launcher_background.xml similarity index 100% rename from android/app/src/main/res/values/ic_launcher_background.xml rename to test-apps/android-test-app/app/src/main/res/values/ic_launcher_background.xml diff --git a/android/app/src/main/res/values/strings.xml b/test-apps/android-test-app/app/src/main/res/values/strings.xml similarity index 100% rename from android/app/src/main/res/values/strings.xml rename to test-apps/android-test-app/app/src/main/res/values/strings.xml diff --git a/android/app/src/main/res/values/styles.xml b/test-apps/android-test-app/app/src/main/res/values/styles.xml similarity index 100% rename from android/app/src/main/res/values/styles.xml rename to test-apps/android-test-app/app/src/main/res/values/styles.xml diff --git a/test-apps/android-test-app/app/src/main/res/xml/config.xml b/test-apps/android-test-app/app/src/main/res/xml/config.xml new file mode 100644 index 0000000..1b1b0e0 --- /dev/null +++ b/test-apps/android-test-app/app/src/main/res/xml/config.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/file_paths.xml b/test-apps/android-test-app/app/src/main/res/xml/file_paths.xml similarity index 100% rename from android/app/src/main/res/xml/file_paths.xml rename to test-apps/android-test-app/app/src/main/res/xml/file_paths.xml diff --git a/android/app/src/main/res/xml/notification_channels.xml b/test-apps/android-test-app/app/src/main/res/xml/notification_channels.xml similarity index 100% rename from android/app/src/main/res/xml/notification_channels.xml rename to test-apps/android-test-app/app/src/main/res/xml/notification_channels.xml diff --git a/android/app/src/test/java/com/timesafari/dailynotification/ExampleUnitTest.java b/test-apps/android-test-app/app/src/test/java/com/timesafari/dailynotification/ExampleUnitTest.java similarity index 100% rename from android/app/src/test/java/com/timesafari/dailynotification/ExampleUnitTest.java rename to test-apps/android-test-app/app/src/test/java/com/timesafari/dailynotification/ExampleUnitTest.java diff --git a/test-apps/android-test-app/build.gradle b/test-apps/android-test-app/build.gradle new file mode 100644 index 0000000..a7caffe --- /dev/null +++ b/test-apps/android-test-app/build.gradle @@ -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 +} + diff --git a/test-apps/android-test-app/gradle.properties b/test-apps/android-test-app/gradle.properties new file mode 100644 index 0000000..780e78b --- /dev/null +++ b/test-apps/android-test-app/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536m +android.useAndroidX=true +android.enableJetifier=true + diff --git a/test-apps/android-test-app/gradle/wrapper/gradle-wrapper.jar b/test-apps/android-test-app/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/test-apps/android-test-app/gradle/wrapper/gradle-wrapper.jar differ diff --git a/test-apps/android-test-app/gradle/wrapper/gradle-wrapper.properties b/test-apps/android-test-app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/test-apps/android-test-app/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/test-apps/android-test-app/gradlew b/test-apps/android-test-app/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/test-apps/android-test-app/gradlew @@ -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" "$@" diff --git a/test-apps/android-test-app/gradlew.bat b/test-apps/android-test-app/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/test-apps/android-test-app/gradlew.bat @@ -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 diff --git a/test-apps/android-test-app/settings.gradle b/test-apps/android-test-app/settings.gradle new file mode 100644 index 0000000..4b42b06 --- /dev/null +++ b/test-apps/android-test-app/settings.gradle @@ -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.") +} + diff --git a/android/variables.gradle b/test-apps/android-test-app/variables.gradle similarity index 100% rename from android/variables.gradle rename to test-apps/android-test-app/variables.gradle diff --git a/test-apps/daily-notification-test/android/capacitor.settings.gradle b/test-apps/daily-notification-test/android/capacitor.settings.gradle index ab7c2c5..586338f 100644 --- a/test-apps/daily-notification-test/android/capacitor.settings.gradle +++ b/test-apps/daily-notification-test/android/capacitor.settings.gradle @@ -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') diff --git a/test-apps/daily-notification-test/scripts/fix-capacitor-plugins.js b/test-apps/daily-notification-test/scripts/fix-capacitor-plugins.js index 277b480..27d6d43 100755 --- a/test-apps/daily-notification-test/scripts/fix-capacitor-plugins.js +++ b/test-apps/daily-notification-test/scripts/fix-capacitor-plugins.js @@ -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); } }