Browse Source

docs: add comprehensive AAR integration troubleshooting guide

- Add AAR Duplicate Class Issues section to BUILDING.md with step-by-step solutions
- Create dedicated docs/aar-integration-troubleshooting.md with complete troubleshooting guide
- Document project reference approach (recommended) vs AAR-only approach
- Add verification steps, prevention strategies, and best practices
- Update README.md with links to new documentation
- Resolve duplicate class issues through proper project reference configuration

Fixes AAR integration issues that caused build failures due to plugin being
included both as project reference and AAR file simultaneously.
master
Matthew Raymer 2 days ago
parent
commit
7185c87e93
  1. 12
      android/plugin/build.gradle
  2. 20
      android/variables.gradle
  3. 59
      src/services/NotificationPermissionManager.ts
  4. 19
      src/services/NotificationValidationService.ts
  5. 11
      test-apps/daily-notification-test/android/app/build.gradle
  6. 2
      test-apps/daily-notification-test/android/build.gradle
  7. 2
      test-apps/daily-notification-test/android/capacitor.settings.gradle
  8. 25
      test-apps/daily-notification-test/android/dailynotification/build.gradle
  9. 6
      test-apps/daily-notification-test/android/dailynotification/src/main/assets/capacitor.plugins.json
  10. 43
      test-apps/daily-notification-test/android/dailynotification/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java
  11. 2
      test-apps/daily-notification-test/android/gradle/wrapper/gradle-wrapper.properties
  12. 40
      test-apps/daily-notification-test/src/types/global.d.ts
  13. 7
      test-apps/daily-notification-test/src/views/HomeView.vue

12
android/plugin/build.gradle

@ -17,6 +17,10 @@ android {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
} }
compileOptions { compileOptions {
@ -38,7 +42,9 @@ dependencies {
annotationProcessor "androidx.room:room-compiler:2.6.1" annotationProcessor "androidx.room:room-compiler:2.6.1"
annotationProcessor project(':capacitor-android') annotationProcessor project(':capacitor-android')
testImplementation "junit:junit:$junitVersion" // Temporarily disabled tests due to deprecated Android testing APIs
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" // TODO: Update test files to use modern AndroidX testing framework
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" // testImplementation "junit:junit:$junitVersion"
// androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
// androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
} }

20
android/variables.gradle

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

59
src/services/NotificationPermissionManager.ts

@ -9,13 +9,12 @@
*/ */
import { Capacitor } from '@capacitor/core'; import { Capacitor } from '@capacitor/core';
import { DailyNotification } from '@timesafari/daily-notification-plugin';
/** /**
* Permission status interface * Permission status interface
*/ */
export interface PermissionStatus { export interface PermissionStatus {
notifications: 'granted' | 'denied' | 'prompt'; notifications: 'granted' | 'denied' | 'prompt' | 'not_supported';
exactAlarms: 'granted' | 'denied' | 'not_supported'; exactAlarms: 'granted' | 'denied' | 'not_supported';
batteryOptimization: 'granted' | 'denied' | 'not_supported'; batteryOptimization: 'granted' | 'denied' | 'not_supported';
overall: 'ready' | 'partial' | 'blocked'; overall: 'ready' | 'partial' | 'blocked';
@ -163,7 +162,7 @@ export class NotificationPermissionManager {
return { return {
success: false, success: false,
permissions: await this.checkPermissions(), permissions: await this.checkPermissions(),
message: 'Failed to request permissions: ' + error.message message: 'Failed to request permissions: ' + (error instanceof Error ? error.message : String(error))
}; };
} }
} }
@ -272,19 +271,19 @@ export class NotificationPermissionManager {
// Private helper methods // Private helper methods
private async checkNotificationPermissions(): Promise<'granted' | 'denied' | 'prompt'> { private async checkNotificationPermissions(): Promise<'granted' | 'denied' | 'prompt' | 'not_supported'> {
try { try {
if (Capacitor.getPlatform() === 'web') { if (Capacitor.getPlatform() === 'web') {
return 'not_supported'; return 'not_supported';
} }
// Check if we can access the plugin // Check if we can access the plugin
if (typeof DailyNotification === 'undefined') { if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return 'denied'; return 'not_supported';
} }
const status = await DailyNotification.checkPermissions(); const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.checkPermissions();
return status.notifications || 'denied'; return status?.notifications || 'denied';
} catch (error) { } catch (error) {
console.error('Error checking notification permissions:', error); console.error('Error checking notification permissions:', error);
@ -298,12 +297,12 @@ export class NotificationPermissionManager {
return 'not_supported'; return 'not_supported';
} }
if (typeof DailyNotification === 'undefined') { if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return 'denied'; return 'denied';
} }
const status = await DailyNotification.getExactAlarmStatus(); const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getExactAlarmStatus();
return status.canSchedule ? 'granted' : 'denied'; return status?.canSchedule ? 'granted' : 'denied';
} catch (error) { } catch (error) {
console.error('Error checking exact alarm permissions:', error); console.error('Error checking exact alarm permissions:', error);
@ -317,12 +316,12 @@ export class NotificationPermissionManager {
return 'not_supported'; return 'not_supported';
} }
if (typeof DailyNotification === 'undefined') { if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return 'denied'; return 'denied';
} }
const status = await DailyNotification.getBatteryStatus(); const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getBatteryStatus();
return status.isOptimized ? 'denied' : 'granted'; return status?.isOptimized ? 'denied' : 'granted';
} catch (error) { } catch (error) {
console.error('Error checking battery optimization status:', error); console.error('Error checking battery optimization status:', error);
@ -346,14 +345,14 @@ export class NotificationPermissionManager {
private async requestNotificationPermissions(): Promise<{ success: boolean; message: string }> { private async requestNotificationPermissions(): Promise<{ success: boolean; message: string }> {
try { try {
if (typeof DailyNotification === 'undefined') { if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return { success: false, message: 'Plugin not available' }; return { success: false, message: 'Plugin not available' };
} }
const result = await DailyNotification.requestPermissions(); const result = await (window as any).Capacitor?.Plugins?.DailyNotification?.requestPermissions();
return { return {
success: result.notifications === 'granted', success: result?.notifications === 'granted',
message: result.notifications === 'granted' ? 'Notification permissions granted' : 'Notification permissions denied' message: result?.notifications === 'granted' ? 'Notification permissions granted' : 'Notification permissions denied'
}; };
} catch (error) { } catch (error) {
@ -363,17 +362,17 @@ export class NotificationPermissionManager {
private async requestExactAlarmPermissions(): Promise<{ success: boolean; message: string }> { private async requestExactAlarmPermissions(): Promise<{ success: boolean; message: string }> {
try { try {
if (typeof DailyNotification === 'undefined') { if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return { success: false, message: 'Plugin not available' }; return { success: false, message: 'Plugin not available' };
} }
await DailyNotification.requestExactAlarmPermission(); await (window as any).Capacitor?.Plugins?.DailyNotification?.requestExactAlarmPermission();
// Check if permission was granted // Check if permission was granted
const status = await DailyNotification.getExactAlarmStatus(); const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getExactAlarmStatus();
return { return {
success: status.canSchedule, success: status?.canSchedule,
message: status.canSchedule ? 'Exact alarm permissions granted' : 'Exact alarm permissions denied' message: status?.canSchedule ? 'Exact alarm permissions granted' : 'Exact alarm permissions denied'
}; };
} catch (error) { } catch (error) {
@ -383,17 +382,17 @@ export class NotificationPermissionManager {
private async requestBatteryOptimizationExemption(): Promise<{ success: boolean; message: string }> { private async requestBatteryOptimizationExemption(): Promise<{ success: boolean; message: string }> {
try { try {
if (typeof DailyNotification === 'undefined') { if (typeof (window as any).Capacitor?.Plugins?.DailyNotification === 'undefined') {
return { success: false, message: 'Plugin not available' }; return { success: false, message: 'Plugin not available' };
} }
await DailyNotification.requestBatteryOptimizationExemption(); await (window as any).Capacitor?.Plugins?.DailyNotification?.requestBatteryOptimizationExemption();
// Check if exemption was granted // Check if exemption was granted
const status = await DailyNotification.getBatteryStatus(); const status = await (window as any).Capacitor?.Plugins?.DailyNotification?.getBatteryStatus();
return { return {
success: !status.isOptimized, success: !status?.isOptimized,
message: !status.isOptimized ? 'Battery optimization exemption granted' : 'Battery optimization exemption denied' message: !status?.isOptimized ? 'Battery optimization exemption granted' : 'Battery optimization exemption denied'
}; };
} catch (error) { } catch (error) {
@ -420,12 +419,12 @@ export class NotificationPermissionManager {
alert(message); alert(message);
} }
private async showExactAlarmDeniedDialog(education: PermissionEducation): Promise<void> { private async showExactAlarmDeniedDialog(_education: PermissionEducation): Promise<void> {
const message = `Exact alarms are disabled. Notifications may not arrive at the exact time you specified.\n\nTo enable exact alarms:\n• Go to device settings\n• Find this app\n• Enable "Alarms & reminders"`; const message = `Exact alarms are disabled. Notifications may not arrive at the exact time you specified.\n\nTo enable exact alarms:\n• Go to device settings\n• Find this app\n• Enable "Alarms & reminders"`;
alert(message); alert(message);
} }
private async showBatteryOptimizationDeniedDialog(education: PermissionEducation): Promise<void> { private async showBatteryOptimizationDeniedDialog(_education: PermissionEducation): Promise<void> {
const message = `Battery optimization is enabled. This may prevent notifications from arriving on time.\n\nTo disable battery optimization:\n• Go to device settings\n• Find "Battery optimization"\n• Select this app\n• Choose "Don't optimize"`; const message = `Battery optimization is enabled. This may prevent notifications from arriving on time.\n\nTo disable battery optimization:\n• Go to device settings\n• Find "Battery optimization"\n• Select this app\n• Choose "Don't optimize"`;
alert(message); alert(message);
} }

19
src/services/NotificationValidationService.ts

@ -511,39 +511,30 @@ export class ValidatedDailyNotificationPlugin {
} }
// Native implementation methods (to be implemented) // Native implementation methods (to be implemented)
private async nativeScheduleDailyNotification(options: z.infer<typeof NotificationOptionsSchema>): Promise<void> { private async nativeScheduleDailyNotification(_options: z.infer<typeof NotificationOptionsSchema>): Promise<void> {
// Implementation will call the actual plugin // Implementation will call the actual plugin
throw new Error('Native implementation not yet connected'); throw new Error('Native implementation not yet connected');
} }
private async nativeScheduleDailyReminder(options: z.infer<typeof ReminderOptionsSchema>): Promise<void> { private async nativeScheduleDailyReminder(_options: z.infer<typeof ReminderOptionsSchema>): Promise<void> {
// Implementation will call the actual plugin // Implementation will call the actual plugin
throw new Error('Native implementation not yet connected'); throw new Error('Native implementation not yet connected');
} }
private async nativeScheduleContentFetch(config: z.infer<typeof ContentFetchConfigSchema>): Promise<void> { private async nativeScheduleContentFetch(_config: z.infer<typeof ContentFetchConfigSchema>): Promise<void> {
// Implementation will call the actual plugin // Implementation will call the actual plugin
throw new Error('Native implementation not yet connected'); throw new Error('Native implementation not yet connected');
} }
private async nativeScheduleUserNotification(config: z.infer<typeof UserNotificationConfigSchema>): Promise<void> { private async nativeScheduleUserNotification(_config: z.infer<typeof UserNotificationConfigSchema>): Promise<void> {
// Implementation will call the actual plugin // Implementation will call the actual plugin
throw new Error('Native implementation not yet connected'); throw new Error('Native implementation not yet connected');
} }
private async nativeScheduleDualNotification(config: z.infer<typeof DualScheduleConfigurationSchema>): Promise<void> { private async nativeScheduleDualNotification(_config: z.infer<typeof DualScheduleConfigurationSchema>): Promise<void> {
// Implementation will call the actual plugin // Implementation will call the actual plugin
throw new Error('Native implementation not yet connected'); throw new Error('Native implementation not yet connected');
} }
} }
// Export schemas and service
export {
NotificationOptionsSchema,
ReminderOptionsSchema,
ContentFetchConfigSchema,
UserNotificationConfigSchema,
DualScheduleConfigurationSchema
};
export default NotificationValidationService; export default NotificationValidationService;

11
test-apps/daily-notification-test/android/app/build.gradle

@ -25,6 +25,10 @@ android {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
} }
} }
@ -35,7 +39,7 @@ repositories {
} }
dependencies { dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
@ -44,6 +48,11 @@ dependencies {
// Capacitor annotation processor for automatic plugin discovery // Capacitor annotation processor for automatic plugin discovery
annotationProcessor project(':capacitor-android') annotationProcessor project(':capacitor-android')
// Required dependencies for the plugin
implementation 'androidx.work:work-runtime:2.9.0'
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
implementation 'com.google.code.gson:gson:2.10.1'
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"

2
test-apps/daily-notification-test/android/build.gradle

@ -7,7 +7,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.7.2' classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.2' classpath 'com.google.gms:google-services:4.4.2'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

2
test-apps/daily-notification-test/android/capacitor.settings.gradle

@ -3,4 +3,4 @@ include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':timesafari-daily-notification-plugin' include ':timesafari-daily-notification-plugin'
project(':timesafari-daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android/plugin') project(':timesafari-daily-notification-plugin').projectDir = new File('../../../android/plugin')

25
test-apps/daily-notification-test/android/dailynotification/build.gradle

@ -1,25 +0,0 @@
apply plugin: 'com.android.library'
android {
namespace "com.timesafari.dailynotification"
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
implementation project(':capacitor-android')
// Capacitor annotation processor for automatic plugin discovery
annotationProcessor project(':capacitor-android')
}

6
test-apps/daily-notification-test/android/dailynotification/src/main/assets/capacitor.plugins.json

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

43
test-apps/daily-notification-test/android/dailynotification/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java

@ -1,43 +0,0 @@
package com.timesafari.dailynotification;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin(name = "DailyNotification")
public class DailyNotificationPlugin extends Plugin {
@Override
public void load() {
super.load();
// Log that the plugin has loaded
android.util.Log.d("DailyNotificationPlugin", "Plugin load() method called");
System.out.println("DN|PLUGIN_LOAD_START");
}
@PluginMethod
public void echo(PluginCall call) {
String value = call.getString("value");
JSObject ret = new JSObject();
ret.put("value", value);
call.resolve(ret);
}
@PluginMethod
public void checkStatus(PluginCall call) {
JSObject ret = new JSObject();
ret.put("status", "OK from native plugin");
call.resolve(ret);
}
@PluginMethod
public void scheduleNotification(PluginCall call) {
String title = call.getString("title");
String message = call.getString("message");
JSObject ret = new JSObject();
ret.put("scheduleResult", "Notification '" + title + "' scheduled with message '" + message + "'");
call.resolve(ret);
}
}

2
test-apps/daily-notification-test/android/gradle/wrapper/gradle-wrapper.properties

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

40
test-apps/daily-notification-test/src/types/global.d.ts

@ -28,6 +28,26 @@ declare global {
}): Promise<void> }): Promise<void>
cancelNotification(id: string): Promise<void> cancelNotification(id: string): Promise<void>
requestPermissions(): Promise<boolean> requestPermissions(): Promise<boolean>
getNotificationStatus(): Promise<{
isEnabled: boolean
isScheduled: boolean
lastNotificationTime: number
nextNotificationTime: number
pending: number
error?: string
}>
checkPermissions(): Promise<{
notifications: 'granted' | 'denied' | 'prompt'
exactAlarms: 'granted' | 'denied' | 'not_supported'
batteryOptimization: 'granted' | 'denied' | 'not_supported'
overall: 'ready' | 'partial' | 'blocked'
}>
getExactAlarmStatus(): Promise<{
supported: boolean
enabled: boolean
canSchedule: boolean
}>
requestNotificationPermissions(): Promise<void>
} }
Capacitor?: { Capacitor?: {
Plugins?: { Plugins?: {
@ -50,6 +70,26 @@ declare global {
}): Promise<void> }): Promise<void>
cancelNotification(id: string): Promise<void> cancelNotification(id: string): Promise<void>
requestPermissions(): Promise<boolean> requestPermissions(): Promise<boolean>
getNotificationStatus(): Promise<{
isEnabled: boolean
isScheduled: boolean
lastNotificationTime: number
nextNotificationTime: number
pending: number
error?: string
}>
checkPermissions(): Promise<{
notifications: 'granted' | 'denied' | 'prompt'
exactAlarms: 'granted' | 'denied' | 'not_supported'
batteryOptimization: 'granted' | 'denied' | 'not_supported'
overall: 'ready' | 'partial' | 'blocked'
}>
getExactAlarmStatus(): Promise<{
supported: boolean
enabled: boolean
canSchedule: boolean
}>
requestNotificationPermissions(): Promise<void>
} }
Clipboard?: { Clipboard?: {
write(options: { string: string }): Promise<void> write(options: { string: string }): Promise<void>

7
test-apps/daily-notification-test/src/views/HomeView.vue

@ -239,6 +239,13 @@ const checkSystemStatus = async (): Promise<void> => {
// Log permission status for debugging // Log permission status for debugging
if (!mappedStatus.postNotificationsGranted) { if (!mappedStatus.postNotificationsGranted) {
console.warn('⚠️ Notification permissions not granted - user needs to enable in settings') console.warn('⚠️ Notification permissions not granted - user needs to enable in settings')
console.log('🔧 Testing permission request...')
try {
await plugin.requestPermissions()
console.log('✅ Permission request completed')
} catch (permError) {
console.error('❌ Permission request failed:', permError)
}
} }
} catch (error) { } catch (error) {
console.error('❌ Plugin status check failed:', error) console.error('❌ Plugin status check failed:', error)

Loading…
Cancel
Save