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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,146 @@
/**
* NativeNotificationContentFetcher.java
*
* Service Provider Interface (SPI) for native content fetchers.
*
* This interface is part of the Integration Point Refactor (PR1) that allows
* host apps to provide their own content fetching logic without hardcoding
* TimeSafari-specific code in the plugin.
*
* Host apps implement this interface in native code (Kotlin/Java) and register
* it with the plugin. The plugin calls this interface from background workers
* (WorkManager) to fetch notification content.
*
* @author Matthew Raymer
* @version 1.0.0
*/
package com.timesafari.dailynotification;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Native content fetcher interface for host app implementations
*
* This interface enables the plugin to call host app's native code for
* fetching notification content. This is the ONLY path used by background
* workers, as JavaScript bridges are unreliable in background contexts.
*
* Implementation Requirements:
* - Must be thread-safe (may be called from WorkManager background threads)
* - Must complete within reasonable time (plugin enforces timeout)
* - Should return empty list on failure rather than throwing exceptions
* - Should handle errors gracefully and log for debugging
*
* Example Implementation:
* <pre>
* class TimeSafariNativeFetcher implements NativeNotificationContentFetcher {
* private final TimeSafariApi api;
* private final TokenProvider tokenProvider;
*
* @Override
* public CompletableFuture<List<NotificationContent>> fetchContent(
* FetchContext context) {
* return CompletableFuture.supplyAsync(() -> {
* try {
* String jwt = tokenProvider.freshToken();
* // Fetch from TimeSafari API
* // Convert to NotificationContent[]
* return notificationContents;
* } catch (Exception e) {
* Log.e("Fetcher", "Fetch failed", e);
* return Collections.emptyList();
* }
* });
* }
* }
* </pre>
*/
public interface NativeNotificationContentFetcher {
/**
* Fetch notification content from external source
*
* This method is called by the plugin when:
* - Background fetch work is triggered (WorkManager)
* - Prefetch is scheduled before notification time
* - Manual refresh is requested (if native fetcher enabled)
*
* The plugin will:
* - Enforce a timeout (default 30 seconds, configurable via SchedulingPolicy)
* - Handle empty lists gracefully (no notifications scheduled)
* - Log errors for debugging
* - Retry on failure based on SchedulingPolicy
*
* @param context Context about why fetch was triggered, including
* trigger type, scheduled time, and optional metadata
* @return CompletableFuture that resolves to list of NotificationContent.
* Empty list indicates no content available (not an error).
* The future should complete exceptionally only on unrecoverable errors.
*/
@NonNull
CompletableFuture<List<NotificationContent>> fetchContent(@NonNull FetchContext context);
/**
* Optional: Configure the native fetcher with API credentials and settings
*
* <p>This method is called by the plugin when {@code configureNativeFetcher} is invoked
* from TypeScript. It provides a cross-platform mechanism for passing configuration
* from the JavaScript layer to native code without using platform-specific storage
* mechanisms.</p>
*
* <p><b>When to implement:</b></p>
* <ul>
* <li>Your fetcher needs API credentials (URL, authentication tokens, etc.)</li>
* <li>Configuration should come from TypeScript/JavaScript code (e.g., from app config)</li>
* <li>You want to avoid hardcoding credentials in native code</li>
* </ul>
*
* <p><b>When to skip (use default no-op):</b></p>
* <ul>
* <li>Your fetcher gets credentials from platform-specific storage (SharedPreferences, Keychain, etc.)</li>
* <li>Your fetcher has hardcoded test credentials</li>
* <li>Configuration is handled internally and doesn't need external input</li>
* </ul>
*
* <p><b>Thread Safety:</b> This method may be called from any thread. Implementations
* must be thread-safe if storing configuration in instance variables.</p>
*
* <p><b>Implementation Pattern:</b></p>
* <pre>{@code
* private volatile String apiBaseUrl;
* private volatile String activeDid;
* private volatile String jwtSecret;
*
* @Override
* public void configure(String apiBaseUrl, String activeDid, String jwtSecret) {
* this.apiBaseUrl = apiBaseUrl;
* this.activeDid = activeDid;
* this.jwtSecret = jwtSecret;
* Log.i(TAG, "Fetcher configured with API: " + apiBaseUrl);
* }
* }</pre>
*
* @param apiBaseUrl Base URL for API server. Examples:
* - Android emulator: "http://10.0.2.2:3000" (maps to host localhost:3000)
* - iOS simulator: "http://localhost:3000"
* - Production: "https://api.timesafari.com"
* @param activeDid Active DID (Decentralized Identifier) for authentication.
* Used as the JWT issuer/subject. Format: "did:ethr:0x..."
* @param jwtToken Pre-generated JWT token (ES256K signed) from TypeScript.
* This token is generated in the host app using TimeSafari's
* {@code createEndorserJwtForKey()} function. The native fetcher
* should use this token directly in the Authorization header as
* "Bearer {jwtToken}". No JWT generation or signing is needed in Java.
*
* @see DailyNotificationPlugin#configureNativeFetcher(PluginCall)
*/
default void configure(String apiBaseUrl, String activeDid, String jwtToken) {
// Default no-op implementation - fetchers that need config can override
// This allows fetchers that don't need TypeScript-provided configuration
// to ignore this method without implementing an empty body.
}
}