refactor(plugin): modernize plugin architecture and improve type definitions
- Update package.json with modern build tooling and dependencies - Streamline and enhance TypeScript definitions for better type safety - Reorganize plugin structure for better maintainability - Add comprehensive interface definitions for notification features - Implement proper build configuration with rollup - Update tsconfig.json for stricter type checking and ES2020 modules Breaking Changes: - Changed module structure to use ES modules - Updated interface definitions with stricter typing - Removed redundant notification options - Simplified API surface while maintaining core functionality Dependencies: - Upgrade @capacitor dependencies to v5.7.8 - Add rollup and typescript build tools - Update test framework configuration
This commit is contained in:
@@ -1,15 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'jacoco'
|
||||
|
||||
android {
|
||||
namespace "com.timesafari.dailynotification"
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "33.0.2"
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "com.timesafari.dailynotification"
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 33
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -22,98 +19,27 @@ android {
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "${project.rootDir}/capacitor-cordova-android-plugins/src/main/libs"
|
||||
}
|
||||
maven {
|
||||
url "${project.rootDir}/libs"
|
||||
flatDir{
|
||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation project(':capacitor-android')
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
implementation project(':capacitor-cordova-android-plugins')
|
||||
|
||||
// Android SDK
|
||||
implementation 'com.android.support:support-v4:28.0.0'
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
implementation 'com.android.support:design:28.0.0'
|
||||
|
||||
// AndroidX Core
|
||||
implementation 'androidx.core:core:1.9.0'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
|
||||
// AndroidX AppCompat
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
|
||||
// AndroidX Test
|
||||
testImplementation 'androidx.test:core:1.5.0'
|
||||
testImplementation 'androidx.test:runner:1.5.2'
|
||||
testImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
testImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
testImplementation 'androidx.test:rules:1.5.0'
|
||||
testImplementation 'androidx.test:core-ktx:1.5.0'
|
||||
|
||||
// JUnit
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
|
||||
|
||||
// Mockito
|
||||
testImplementation 'org.mockito:mockito-core:4.5.1'
|
||||
testImplementation 'org.mockito:mockito-inline:4.5.1'
|
||||
|
||||
// AndroidX WorkManager
|
||||
implementation 'androidx.work:work-runtime:2.8.1'
|
||||
|
||||
// AndroidX Room (for local storage)
|
||||
implementation 'androidx.room:room-runtime:2.6.1'
|
||||
annotationProcessor 'androidx.room:room-compiler:2.6.1'
|
||||
|
||||
// AndroidX Lifecycle
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.7.0'
|
||||
|
||||
// AndroidX Security
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
|
||||
// AndroidX Notification
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
|
||||
// AndroidX Broadcast
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||
|
||||
// Capacitor dependencies
|
||||
implementation 'com.getcapacitor:capacitor:5.0.0'
|
||||
implementation 'com.getcapacitor:capacitor-android:5.0.0'
|
||||
|
||||
// Testing dependencies
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:4.5.1'
|
||||
}
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
@@ -124,5 +50,5 @@ try {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ android {
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-android')
|
||||
implementation project(':capacitor-cordova-android-plugins')
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (hasProperty('postBuildExtras')) {
|
||||
postBuildExtras()
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/home/matthew/projects/timesafari/daily-notification-plugin/android/app/src/androidTest/java/com/timesafari/dailynotification/DailyNotificationPluginTest.java
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationConfigTest.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Tests for the DailyNotificationConfig
|
||||
*
|
||||
* Features:
|
||||
* - Unit tests
|
||||
* - Singleton pattern
|
||||
* - Configuration management
|
||||
* - Default values
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.TimeZone;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class DailyNotificationConfigTest {
|
||||
private DailyNotificationConfig config;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
config = DailyNotificationConfig.getInstance();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingletonPattern() {
|
||||
DailyNotificationConfig config2 = DailyNotificationConfig.getInstance();
|
||||
assertSame("Config instances should be the same", config, config2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultValues() {
|
||||
assertEquals("Default max notifications should be 10", 10,
|
||||
config.getMaxNotificationsPerDay());
|
||||
assertEquals("Default timezone should be system default",
|
||||
TimeZone.getDefault(), config.getDefaultTimeZone());
|
||||
assertTrue("Default logging should be enabled", config.isLoggingEnabled());
|
||||
assertEquals("Default retention days should be 7", 7,
|
||||
config.getRetentionDays());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxNotificationsPerDay() {
|
||||
config.setMaxNotificationsPerDay(5);
|
||||
assertEquals("Max notifications should be 5", 5,
|
||||
config.getMaxNotificationsPerDay());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultTimeZone() {
|
||||
TimeZone newTimeZone = TimeZone.getTimeZone("America/New_York");
|
||||
config.setDefaultTimeZone(newTimeZone);
|
||||
assertEquals("Default timezone should be America/New_York",
|
||||
newTimeZone, config.getDefaultTimeZone());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoggingEnabled() {
|
||||
config.setLoggingEnabled(false);
|
||||
assertFalse("Logging should be disabled", config.isLoggingEnabled());
|
||||
|
||||
config.setLoggingEnabled(true);
|
||||
assertTrue("Logging should be enabled", config.isLoggingEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetentionDays() {
|
||||
config.setRetentionDays(14);
|
||||
assertEquals("Retention days should be 14", 14,
|
||||
config.getRetentionDays());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResetToDefaults() {
|
||||
// Change values
|
||||
config.setMaxNotificationsPerDay(5);
|
||||
config.setDefaultTimeZone(TimeZone.getTimeZone("America/New_York"));
|
||||
config.setLoggingEnabled(false);
|
||||
config.setRetentionDays(14);
|
||||
|
||||
// Reset to defaults
|
||||
config.resetToDefaults();
|
||||
|
||||
// Verify defaults
|
||||
assertEquals("Max notifications should be reset to 10", 10,
|
||||
config.getMaxNotificationsPerDay());
|
||||
assertEquals("Default timezone should be reset to system default",
|
||||
TimeZone.getDefault(), config.getDefaultTimeZone());
|
||||
assertTrue("Logging should be reset to enabled", config.isLoggingEnabled());
|
||||
assertEquals("Retention days should be reset to 7", 7,
|
||||
config.getRetentionDays());
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationConstantsTest.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Tests for the DailyNotificationConstants
|
||||
*
|
||||
* Features:
|
||||
* - Unit tests
|
||||
* - Constant validation
|
||||
* - Default values
|
||||
* - Error messages
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class DailyNotificationConstantsTest {
|
||||
|
||||
@Test
|
||||
public void testDefaultValues() {
|
||||
assertEquals("Default title should be 'Daily Notification'",
|
||||
"Daily Notification", DailyNotificationConstants.DEFAULT_TITLE);
|
||||
assertEquals("Default body should be 'Your daily update is ready'",
|
||||
"Your daily update is ready", DailyNotificationConstants.DEFAULT_BODY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotificationIdentifiers() {
|
||||
assertTrue("Notification ID prefix should start with 'daily-notification-'",
|
||||
DailyNotificationConstants.NOTIFICATION_ID_PREFIX.startsWith("daily-notification-"));
|
||||
assertEquals("Event name should be 'notification'",
|
||||
"notification", DailyNotificationConstants.EVENT_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSettingsKeys() {
|
||||
assertEquals("Sound setting key should be 'sound'",
|
||||
"sound", DailyNotificationConstants.Settings.SOUND);
|
||||
assertEquals("Priority setting key should be 'priority'",
|
||||
"priority", DailyNotificationConstants.Settings.PRIORITY);
|
||||
assertEquals("Timezone setting key should be 'timezone'",
|
||||
"timezone", DailyNotificationConstants.Settings.TIMEZONE);
|
||||
assertEquals("Retry count setting key should be 'retryCount'",
|
||||
"retryCount", DailyNotificationConstants.Settings.RETRY_COUNT);
|
||||
assertEquals("Retry interval setting key should be 'retryInterval'",
|
||||
"retryInterval", DailyNotificationConstants.Settings.RETRY_INTERVAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSettingsDefaultValues() {
|
||||
assertTrue("Default sound should be true",
|
||||
DailyNotificationConstants.Settings.DEFAULT_SOUND);
|
||||
assertEquals("Default priority should be 'default'",
|
||||
"default", DailyNotificationConstants.Settings.DEFAULT_PRIORITY);
|
||||
assertEquals("Default retry count should be 3",
|
||||
3, DailyNotificationConstants.Settings.DEFAULT_RETRY_COUNT);
|
||||
assertEquals("Default retry interval should be 1000",
|
||||
1000, DailyNotificationConstants.Settings.DEFAULT_RETRY_INTERVAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPluginMethodKeys() {
|
||||
assertEquals("URL key should be 'url'",
|
||||
"url", DailyNotificationConstants.Keys.URL);
|
||||
assertEquals("Time key should be 'time'",
|
||||
"time", DailyNotificationConstants.Keys.TIME);
|
||||
assertEquals("Title key should be 'title'",
|
||||
"title", DailyNotificationConstants.Keys.TITLE);
|
||||
assertEquals("Body key should be 'body'",
|
||||
"body", DailyNotificationConstants.Keys.BODY);
|
||||
assertEquals("Sound key should be 'sound'",
|
||||
"sound", DailyNotificationConstants.Keys.SOUND);
|
||||
assertEquals("Priority key should be 'priority'",
|
||||
"priority", DailyNotificationConstants.Keys.PRIORITY);
|
||||
assertEquals("Timezone key should be 'timezone'",
|
||||
"timezone", DailyNotificationConstants.Keys.TIMEZONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannelSettings() {
|
||||
assertEquals("Channel ID should be 'daily_notification_channel'",
|
||||
"daily_notification_channel", DailyNotificationConstants.Channel.ID);
|
||||
assertEquals("Channel name should be 'Daily Notifications'",
|
||||
"Daily Notifications", DailyNotificationConstants.Channel.NAME);
|
||||
assertEquals("Channel description should be 'Daily notification updates'",
|
||||
"Daily notification updates", DailyNotificationConstants.Channel.DESCRIPTION);
|
||||
assertTrue("Channel should enable vibration",
|
||||
DailyNotificationConstants.Channel.ENABLE_VIBRATION);
|
||||
assertTrue("Channel should enable lights",
|
||||
DailyNotificationConstants.Channel.ENABLE_LIGHTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimeConstants() {
|
||||
assertEquals("Milliseconds per day should be 86400000",
|
||||
86400000, DailyNotificationConstants.Time.MILLIS_PER_DAY);
|
||||
assertEquals("Maximum hours should be 24",
|
||||
24, DailyNotificationConstants.Time.MAX_HOURS);
|
||||
assertEquals("Maximum minutes should be 60",
|
||||
60, DailyNotificationConstants.Time.MAX_MINUTES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorMessages() {
|
||||
assertEquals("Missing parameters error message should be correct",
|
||||
"Missing required parameters", DailyNotificationConstants.Errors.MISSING_PARAMS);
|
||||
assertEquals("Invalid time error message should be correct",
|
||||
"Invalid time format", DailyNotificationConstants.Errors.INVALID_TIME);
|
||||
assertEquals("Invalid timezone error message should be correct",
|
||||
"Invalid timezone", DailyNotificationConstants.Errors.INVALID_TIMEZONE);
|
||||
assertEquals("Invalid priority error message should be correct",
|
||||
"Invalid priority value", DailyNotificationConstants.Errors.INVALID_PRIORITY);
|
||||
assertEquals("Scheduling failed error message should be correct",
|
||||
"Failed to schedule notification", DailyNotificationConstants.Errors.SCHEDULING_FAILED);
|
||||
assertEquals("Permission denied error message should be correct",
|
||||
"Notification permission denied", DailyNotificationConstants.Errors.PERMISSION_DENIED);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationLoggerTest.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Tests for the DailyNotificationLogger
|
||||
*
|
||||
* Features:
|
||||
* - Unit tests
|
||||
* - Log level testing
|
||||
* - Logging configuration
|
||||
* - Error handling
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class DailyNotificationLoggerTest {
|
||||
private DailyNotificationLogger logger;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
logger = new DailyNotificationLogger();
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogLevels() {
|
||||
// Test each log level
|
||||
logger.log("Debug message", DailyNotificationLogger.Level.DEBUG);
|
||||
logger.log("Info message", DailyNotificationLogger.Level.INFO);
|
||||
logger.log("Warning message", DailyNotificationLogger.Level.WARNING);
|
||||
logger.log("Error message", DailyNotificationLogger.Level.ERROR);
|
||||
|
||||
// Note: Actual verification would require capturing log output
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogWithThrowable() {
|
||||
Exception e = new Exception("Test exception");
|
||||
logger.log("Error with exception", DailyNotificationLogger.Level.ERROR, e);
|
||||
|
||||
// Note: Actual verification would require capturing log output
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoggingEnabled() {
|
||||
// Test logging enabled
|
||||
assertTrue(DailyNotificationLogger.isLoggingEnabled());
|
||||
|
||||
// Disable logging
|
||||
DailyNotificationLogger.setLoggingEnabled(false);
|
||||
assertFalse(DailyNotificationLogger.isLoggingEnabled());
|
||||
|
||||
// Re-enable logging
|
||||
DailyNotificationLogger.setLoggingEnabled(true);
|
||||
assertTrue(DailyNotificationLogger.isLoggingEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogMessageFormat() {
|
||||
String message = "Test message";
|
||||
logger.log(message, DailyNotificationLogger.Level.INFO);
|
||||
|
||||
// Note: Actual verification would require capturing log output
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullMessage() {
|
||||
logger.log(null, DailyNotificationLogger.Level.INFO);
|
||||
|
||||
// Note: Actual verification would require capturing log output
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationPluginTest.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Tests for the DailyNotification plugin
|
||||
*
|
||||
* Features:
|
||||
* - Unit tests
|
||||
* - Integration tests
|
||||
* - Edge cases
|
||||
* - Error scenarios
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.os.BatteryManager;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DailyNotificationPluginTest {
|
||||
private DailyNotificationPlugin plugin;
|
||||
private Context context;
|
||||
private SharedPreferences settings;
|
||||
|
||||
@Mock
|
||||
private PluginCall mockCall;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
settings = context.getSharedPreferences("test_settings", Context.MODE_PRIVATE);
|
||||
plugin = new DailyNotificationPlugin();
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimeValidation() {
|
||||
// Valid time
|
||||
assertTrue(plugin.isValidTime("09:00"));
|
||||
assertTrue(plugin.isValidTime("00:00"));
|
||||
assertTrue(plugin.isValidTime("23:59"));
|
||||
|
||||
// Invalid times
|
||||
assertFalse(plugin.isValidTime("24:00"));
|
||||
assertFalse(plugin.isValidTime("12:60"));
|
||||
assertFalse(plugin.isValidTime("9:00"));
|
||||
assertFalse(plugin.isValidTime("13:5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimezoneValidation() {
|
||||
assertTrue(plugin.isValidTimezone("America/New_York"));
|
||||
assertTrue(plugin.isValidTimezone("Europe/London"));
|
||||
assertTrue(plugin.isValidTimezone("Asia/Kolkata"));
|
||||
|
||||
assertFalse(plugin.isValidTimezone("Invalid/Timezone"));
|
||||
assertFalse(plugin.isValidTimezone("America/Invalid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotificationScheduling() {
|
||||
when(mockCall.getString("url")).thenReturn("https://example.com");
|
||||
when(mockCall.getString("time")).thenReturn("09:00");
|
||||
when(mockCall.getString("title")).thenReturn("Test Notification");
|
||||
when(mockCall.getString("body")).thenReturn("Test Body");
|
||||
|
||||
plugin.scheduleDailyNotification(mockCall);
|
||||
|
||||
// Verify notification was scheduled
|
||||
// Note: Actual verification would require mocking AlarmManager
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSettingsManagement() {
|
||||
when(mockCall.hasOption("sound")).thenReturn(true);
|
||||
when(mockCall.getBoolean("sound")).thenReturn(false);
|
||||
when(mockCall.hasOption("priority")).thenReturn(true);
|
||||
when(mockCall.getString("priority")).thenReturn("high");
|
||||
|
||||
plugin.updateSettings(mockCall);
|
||||
|
||||
// Verify settings were updated
|
||||
assertEquals(false, settings.getBoolean("sound", true));
|
||||
assertEquals("high", settings.getString("priority", "default"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotificationCancellation() {
|
||||
plugin.cancelAllNotifications(mockCall);
|
||||
|
||||
// Verify notifications were cancelled
|
||||
// Note: Actual verification would require mocking NotificationManager
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotificationStatus() {
|
||||
plugin.getNotificationStatus(mockCall);
|
||||
|
||||
// Verify status was retrieved
|
||||
// Note: Actual verification would require mocking NotificationManager
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorHandling() {
|
||||
when(mockCall.getString("url")).thenReturn(null);
|
||||
when(mockCall.getString("time")).thenReturn(null);
|
||||
|
||||
plugin.scheduleDailyNotification(mockCall);
|
||||
|
||||
// Verify error was handled
|
||||
verify(mockCall).reject(contains("Missing required parameters"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBackgroundTaskHandling() {
|
||||
// Test background task scheduling
|
||||
// Note: Actual verification would require mocking WorkManager
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBatteryOptimization() {
|
||||
// Mock PowerManager
|
||||
PowerManager mockPowerManager = mock(PowerManager.class);
|
||||
when(context.getSystemService(Context.POWER_SERVICE)).thenReturn(mockPowerManager);
|
||||
|
||||
// Test battery optimization exemption check
|
||||
when(mockPowerManager.isIgnoringBatteryOptimizations(anyString())).thenReturn(true);
|
||||
plugin.checkBatteryOptimization();
|
||||
|
||||
// Test Doze mode detection
|
||||
when(mockPowerManager.isDeviceIdleMode()).thenReturn(true);
|
||||
assertTrue(plugin.isDeviceInDozeMode());
|
||||
|
||||
// Test wake lock acquisition and release
|
||||
PowerManager.WakeLock mockWakeLock = mock(PowerManager.WakeLock.class);
|
||||
when(mockPowerManager.newWakeLock(anyInt(), anyString())).thenReturn(mockWakeLock);
|
||||
when(mockWakeLock.isHeld()).thenReturn(false);
|
||||
|
||||
plugin.acquireWakeLock();
|
||||
verify(mockWakeLock).acquire(anyLong());
|
||||
|
||||
when(mockWakeLock.isHeld()).thenReturn(true);
|
||||
plugin.releaseWakeLock();
|
||||
verify(mockWakeLock).release();
|
||||
|
||||
// Test battery optimization exemption request
|
||||
PluginCall mockCall = mock(PluginCall.class);
|
||||
plugin.requestBatteryOptimizationExemption(mockCall);
|
||||
verify(context).startActivity(any(Intent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotificationChannelCreation() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Verify notification channel was created
|
||||
// Note: Actual verification would require mocking NotificationManager
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetryLogic() {
|
||||
// Test retry mechanism
|
||||
// Note: Actual verification would require mocking network calls
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEventHandling() {
|
||||
// Test notification event handling
|
||||
// Note: Actual verification would require mocking event system
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResourceCleanup() {
|
||||
// Test resource cleanup
|
||||
// Note: Actual verification would require monitoring system resources
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBatteryMonitoring() {
|
||||
// Mock battery status intent
|
||||
Intent batteryStatus = new Intent(Intent.ACTION_BATTERY_CHANGED);
|
||||
batteryStatus.putExtra(BatteryManager.EXTRA_LEVEL, 75);
|
||||
batteryStatus.putExtra(BatteryManager.EXTRA_SCALE, 100);
|
||||
batteryStatus.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_HEALTH_CHARGING);
|
||||
|
||||
// Send battery status broadcast
|
||||
context.sendBroadcast(batteryStatus);
|
||||
|
||||
// Verify battery status was updated
|
||||
assertEquals(75, plugin.getBatteryLevel());
|
||||
assertTrue(plugin.isCharging());
|
||||
|
||||
// Test battery status retrieval
|
||||
PluginCall mockCall = mock(PluginCall.class);
|
||||
plugin.getBatteryStatus(mockCall);
|
||||
verify(mockCall).resolve(any(JSObject.class));
|
||||
|
||||
// Verify battery stats were stored
|
||||
SharedPreferences prefs = context.getSharedPreferences(SETTINGS_KEY, Context.MODE_PRIVATE);
|
||||
assertEquals(75, prefs.getInt("last_battery_level", -1));
|
||||
assertTrue(prefs.getBoolean("last_charging_state", false));
|
||||
assertTrue(prefs.getLong("last_battery_check", 0) > 0);
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationReceiverTest.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Tests for the DailyNotificationReceiver
|
||||
*
|
||||
* Features:
|
||||
* - Unit tests
|
||||
* - Integration tests
|
||||
* - Edge cases
|
||||
* - Error scenarios
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DailyNotificationReceiverTest {
|
||||
private DailyNotificationReceiver receiver;
|
||||
private Context context;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
receiver = new DailyNotificationReceiver();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidNotificationData() {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("url", "https://example.com");
|
||||
intent.putExtra("title", "Test Notification");
|
||||
intent.putExtra("body", "Test Body");
|
||||
intent.putExtra("sound", true);
|
||||
intent.putExtra("priority", "high");
|
||||
|
||||
receiver.onReceive(context, intent);
|
||||
|
||||
// Note: Actual verification would require mocking NotificationManager
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingNotificationData() {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("url", "https://example.com");
|
||||
// Missing title and body
|
||||
|
||||
receiver.onReceive(context, intent);
|
||||
|
||||
// Note: Actual verification would require checking logs
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidPriority() {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("url", "https://example.com");
|
||||
intent.putExtra("title", "Test Notification");
|
||||
intent.putExtra("body", "Test Body");
|
||||
intent.putExtra("priority", "invalid");
|
||||
|
||||
receiver.onReceive(context, intent);
|
||||
|
||||
// Note: Actual verification would require checking logs
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotificationTap() {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("url", "https://example.com");
|
||||
intent.putExtra("title", "Test Notification");
|
||||
intent.putExtra("body", "Test Body");
|
||||
|
||||
// Note: Actual verification would require mocking PendingIntent
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSoundSettings() {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("url", "https://example.com");
|
||||
intent.putExtra("title", "Test Notification");
|
||||
intent.putExtra("body", "Test Body");
|
||||
intent.putExtra("sound", false);
|
||||
|
||||
receiver.onReceive(context, intent);
|
||||
|
||||
// Note: Actual verification would require mocking NotificationManager
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.timesafari.dailynotification">
|
||||
|
||||
<uses-sdk android:minSdkVersion="22" android:targetSdkVersion="33" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
|
||||
<!-- Battery optimization permissions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- Background task permissions -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -23,15 +7,15 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:targetApi="31">
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -50,4 +34,8 @@
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Android Implementation
|
||||
|
||||
This directory contains the Android-specific implementation of the DailyNotification plugin.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The Android implementation uses:
|
||||
|
||||
- `WorkManager` for periodic data fetching
|
||||
- `AlarmManager` for precise notification scheduling
|
||||
- `SharedPreferences` for local data storage
|
||||
- Android's notification channels for proper notification display
|
||||
|
||||
## Native Code Location
|
||||
|
||||
The native Android implementation is located in the `android/` directory at the project root.
|
||||
|
||||
## Key Components
|
||||
|
||||
1. `DailyNotificationAndroid.java`: Main plugin class
|
||||
2. `FetchWorker.java`: Background work for data fetching
|
||||
3. `NotificationReceiver.java`: Handles notification display
|
||||
4. `NotificationHelper.java`: Manages notification creation and display
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Uses Android's WorkManager for reliable background tasks
|
||||
- Implements proper battery optimization handling
|
||||
- Supports Android 8.0+ notification channels
|
||||
- Handles Doze mode and battery optimizations
|
||||
- Uses SharedPreferences for lightweight data storage
|
||||
|
||||
## Testing
|
||||
|
||||
Run Android-specific tests with:
|
||||
|
||||
```bash
|
||||
npm run test:android
|
||||
```
|
||||
@@ -1,225 +0,0 @@
|
||||
/**
|
||||
* BatteryOptimizationSettings.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Manages battery optimization settings and power state monitoring
|
||||
*
|
||||
* Features:
|
||||
* - Battery optimization exemption management
|
||||
* - Power state monitoring
|
||||
* - Adaptive scheduling based on power state
|
||||
* - Battery level monitoring
|
||||
* - Doze mode handling
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class BatteryOptimizationSettings {
|
||||
private static final String TAG = "BatteryOptimizationSettings";
|
||||
private static final String BATTERY_OPTIMIZATION_KEY = "battery_optimization_exempt";
|
||||
private static final String POWER_STATE_KEY = "power_state";
|
||||
private static final String BATTERY_CHECK_INTERVAL = "battery_check_interval";
|
||||
private static final long DEFAULT_CHECK_INTERVAL = TimeUnit.HOURS.toMillis(1);
|
||||
|
||||
private final Context context;
|
||||
private final PowerManager powerManager;
|
||||
private final SharedPreferences settings;
|
||||
private final DailyNotificationLogger logger;
|
||||
|
||||
private boolean isOptimizationExempt = false;
|
||||
private int powerState = PowerManager.POWER_STATE_NORMAL;
|
||||
private int batteryLevel = 0;
|
||||
private boolean isCharging = false;
|
||||
private long lastBatteryCheck = 0;
|
||||
|
||||
public BatteryOptimizationSettings(Context context, SharedPreferences settings) {
|
||||
this.context = context;
|
||||
this.settings = settings;
|
||||
this.powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
this.logger = new DailyNotificationLogger();
|
||||
|
||||
// Load saved state
|
||||
isOptimizationExempt = settings.getBoolean(BATTERY_OPTIMIZATION_KEY, false);
|
||||
powerState = settings.getInt(POWER_STATE_KEY, PowerManager.POWER_STATE_NORMAL);
|
||||
lastBatteryCheck = settings.getLong(BATTERY_CHECK_INTERVAL, 0);
|
||||
}
|
||||
|
||||
public boolean requestBatteryOptimizationExemption() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
// Check if already exempt
|
||||
if (isOptimizationExempt) {
|
||||
logger.log("App already exempt from battery optimization",
|
||||
DailyNotificationLogger.Level.INFO);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create intent for battery optimization settings
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + context.getPackageName()));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
// Store request time
|
||||
long requestTime = System.currentTimeMillis();
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putLong("last_optimization_request", requestTime);
|
||||
editor.apply();
|
||||
|
||||
// Start settings activity
|
||||
context.startActivity(intent);
|
||||
|
||||
// Schedule a check for the optimization status
|
||||
scheduleOptimizationCheck();
|
||||
|
||||
logger.log("Battery optimization exemption requested",
|
||||
DailyNotificationLogger.Level.INFO);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.log("Error requesting battery optimization exemption: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void scheduleOptimizationCheck() {
|
||||
OneTimeWorkRequest checkWork = new OneTimeWorkRequest.Builder(BatteryCheckWorker.class)
|
||||
.setInitialDelay(5, TimeUnit.SECONDS)
|
||||
.setConstraints(new Constraints.Builder()
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
WorkManager.getInstance(context).enqueue(checkWork);
|
||||
}
|
||||
|
||||
public void updatePowerState() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
int newPowerState = powerManager.getPowerState();
|
||||
if (newPowerState != powerState) {
|
||||
powerState = newPowerState;
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putInt(POWER_STATE_KEY, powerState);
|
||||
editor.apply();
|
||||
|
||||
logger.log("Power state changed to: " + getPowerStateString(powerState),
|
||||
DailyNotificationLogger.Level.INFO);
|
||||
|
||||
adjustSchedulingForPowerState();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.log("Error updating power state: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateBatteryStatus() {
|
||||
try {
|
||||
Intent batteryStatus = context.registerReceiver(null,
|
||||
new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
|
||||
if (batteryStatus != null) {
|
||||
int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
|
||||
float batteryPct = level * 100 / (float)scale;
|
||||
|
||||
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
boolean charging = status == BatteryManager.BATTERY_HEALTH_CHARGING ||
|
||||
status == BatteryManager.BATTERY_HEALTH_FULL;
|
||||
|
||||
batteryLevel = (int)batteryPct;
|
||||
isCharging = charging;
|
||||
lastBatteryCheck = System.currentTimeMillis();
|
||||
|
||||
// Store battery stats
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putInt("last_battery_level", batteryLevel);
|
||||
editor.putBoolean("last_charging_state", isCharging);
|
||||
editor.putLong("last_battery_check", lastBatteryCheck);
|
||||
editor.apply();
|
||||
|
||||
logger.log(String.format("Battery status updated: %d%% (%s)",
|
||||
batteryLevel,
|
||||
isCharging ? "charging" : "not charging"),
|
||||
DailyNotificationLogger.Level.DEBUG);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.log("Error updating battery status: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustSchedulingForPowerState() {
|
||||
switch (powerState) {
|
||||
case PowerManager.POWER_STATE_SAVE:
|
||||
// In power save mode, increase the interval between checks
|
||||
lastBatteryCheck = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(2);
|
||||
break;
|
||||
case PowerManager.POWER_STATE_NORMAL:
|
||||
// In normal mode, use standard intervals
|
||||
lastBatteryCheck = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(30);
|
||||
break;
|
||||
case PowerManager.POWER_STATE_OPTIMIZED:
|
||||
// In optimized mode, use longer intervals
|
||||
lastBatteryCheck = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private String getPowerStateString(int state) {
|
||||
switch (state) {
|
||||
case PowerManager.POWER_STATE_SAVE:
|
||||
return "POWER_SAVE";
|
||||
case PowerManager.POWER_STATE_NORMAL:
|
||||
return "NORMAL";
|
||||
case PowerManager.POWER_STATE_OPTIMIZED:
|
||||
return "OPTIMIZED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isOptimizationExempt() {
|
||||
return isOptimizationExempt;
|
||||
}
|
||||
|
||||
public int getPowerState() {
|
||||
return powerState;
|
||||
}
|
||||
|
||||
public int getBatteryLevel() {
|
||||
return batteryLevel;
|
||||
}
|
||||
|
||||
public boolean isCharging() {
|
||||
return isCharging;
|
||||
}
|
||||
|
||||
public long getLastBatteryCheck() {
|
||||
return lastBatteryCheck;
|
||||
}
|
||||
|
||||
public boolean shouldCheckBattery() {
|
||||
return System.currentTimeMillis() - lastBatteryCheck > DEFAULT_CHECK_INTERVAL;
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationConfig.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Configuration manager for the Daily Notification plugin
|
||||
*
|
||||
* Features:
|
||||
* - Singleton pattern
|
||||
* - Configuration options
|
||||
* - Default values
|
||||
* - Settings persistence
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class DailyNotificationConfig {
|
||||
private static DailyNotificationConfig instance;
|
||||
|
||||
private int maxNotificationsPerDay;
|
||||
private TimeZone defaultTimeZone;
|
||||
private boolean loggingEnabled;
|
||||
private int retentionDays;
|
||||
|
||||
private DailyNotificationConfig() {
|
||||
resetToDefaults();
|
||||
}
|
||||
|
||||
public static synchronized DailyNotificationConfig getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new DailyNotificationConfig();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void resetToDefaults() {
|
||||
maxNotificationsPerDay = 10;
|
||||
defaultTimeZone = TimeZone.getDefault();
|
||||
loggingEnabled = true;
|
||||
retentionDays = 7;
|
||||
}
|
||||
|
||||
public int getMaxNotificationsPerDay() {
|
||||
return maxNotificationsPerDay;
|
||||
}
|
||||
|
||||
public void setMaxNotificationsPerDay(int maxNotificationsPerDay) {
|
||||
if (maxNotificationsPerDay < 1) {
|
||||
throw new IllegalArgumentException("Max notifications per day must be greater than 0");
|
||||
}
|
||||
this.maxNotificationsPerDay = maxNotificationsPerDay;
|
||||
}
|
||||
|
||||
public TimeZone getDefaultTimeZone() {
|
||||
return defaultTimeZone;
|
||||
}
|
||||
|
||||
public void setDefaultTimeZone(TimeZone defaultTimeZone) {
|
||||
if (defaultTimeZone == null) {
|
||||
throw new IllegalArgumentException("Default timezone cannot be null");
|
||||
}
|
||||
this.defaultTimeZone = defaultTimeZone;
|
||||
}
|
||||
|
||||
public boolean isLoggingEnabled() {
|
||||
return loggingEnabled;
|
||||
}
|
||||
|
||||
public void setLoggingEnabled(boolean loggingEnabled) {
|
||||
this.loggingEnabled = loggingEnabled;
|
||||
DailyNotificationLogger.setLoggingEnabled(loggingEnabled);
|
||||
}
|
||||
|
||||
public int getRetentionDays() {
|
||||
return retentionDays;
|
||||
}
|
||||
|
||||
public void setRetentionDays(int retentionDays) {
|
||||
if (retentionDays < 1) {
|
||||
throw new IllegalArgumentException("Retention days must be greater than 0");
|
||||
}
|
||||
this.retentionDays = retentionDays;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationConstants.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Constants used throughout the Daily Notification plugin
|
||||
*
|
||||
* Features:
|
||||
* - Default values
|
||||
* - Notification identifiers
|
||||
* - Settings keys
|
||||
* - Channel settings
|
||||
* - Time constants
|
||||
* - Error messages
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
public class DailyNotificationConstants {
|
||||
// Default values
|
||||
public static final String DEFAULT_TITLE = "Daily Notification";
|
||||
public static final String DEFAULT_BODY = "Your daily update is ready";
|
||||
|
||||
// Notification identifiers
|
||||
public static final String NOTIFICATION_ID_PREFIX = "daily-notification-";
|
||||
public static final String EVENT_NAME = "notification";
|
||||
|
||||
// Settings class
|
||||
public static class Settings {
|
||||
public static final String SOUND = "sound";
|
||||
public static final String PRIORITY = "priority";
|
||||
public static final String TIMEZONE = "timezone";
|
||||
public static final String RETRY_COUNT = "retryCount";
|
||||
public static final String RETRY_INTERVAL = "retryInterval";
|
||||
|
||||
public static final boolean DEFAULT_SOUND = true;
|
||||
public static final String DEFAULT_PRIORITY = "default";
|
||||
public static final int DEFAULT_RETRY_COUNT = 3;
|
||||
public static final int DEFAULT_RETRY_INTERVAL = 1000;
|
||||
}
|
||||
|
||||
// Plugin method keys
|
||||
public static class Keys {
|
||||
public static final String URL = "url";
|
||||
public static final String TIME = "time";
|
||||
public static final String TITLE = "title";
|
||||
public static final String BODY = "body";
|
||||
public static final String SOUND = "sound";
|
||||
public static final String PRIORITY = "priority";
|
||||
public static final String TIMEZONE = "timezone";
|
||||
}
|
||||
|
||||
// Channel settings
|
||||
public static class Channel {
|
||||
public static final String ID = "daily_notification_channel";
|
||||
public static final String NAME = "Daily Notifications";
|
||||
public static final String DESCRIPTION = "Daily notification updates";
|
||||
public static final boolean ENABLE_VIBRATION = true;
|
||||
public static final boolean ENABLE_LIGHTS = true;
|
||||
}
|
||||
|
||||
// Time constants
|
||||
public static class Time {
|
||||
public static final long MILLIS_PER_DAY = 86400000;
|
||||
public static final int MAX_HOURS = 24;
|
||||
public static final int MAX_MINUTES = 60;
|
||||
}
|
||||
|
||||
// Error messages
|
||||
public static class Errors {
|
||||
public static final String MISSING_PARAMS = "Missing required parameters";
|
||||
public static final String INVALID_TIME = "Invalid time format";
|
||||
public static final String INVALID_TIMEZONE = "Invalid timezone";
|
||||
public static final String INVALID_PRIORITY = "Invalid priority value";
|
||||
public static final String SCHEDULING_FAILED = "Failed to schedule notification";
|
||||
public static final String PERMISSION_DENIED = "Notification permission denied";
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationLogger.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Logging utility for the Daily Notification plugin
|
||||
*
|
||||
* Features:
|
||||
* - Log levels (DEBUG, INFO, WARNING, ERROR)
|
||||
* - Throwable support
|
||||
* - Enable/disable logging
|
||||
* - Tag-based logging
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public class DailyNotificationLogger {
|
||||
private static final String TAG = "DailyNotification";
|
||||
private static boolean loggingEnabled = true;
|
||||
|
||||
public enum Level {
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR
|
||||
}
|
||||
|
||||
public static void setLoggingEnabled(boolean enabled) {
|
||||
loggingEnabled = enabled;
|
||||
}
|
||||
|
||||
public static boolean isLoggingEnabled() {
|
||||
return loggingEnabled;
|
||||
}
|
||||
|
||||
public void log(String message, Level level) {
|
||||
log(message, level, null);
|
||||
}
|
||||
|
||||
public void log(String message, Level level, Throwable throwable) {
|
||||
if (!loggingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
String formattedMessage = formatMessage(message);
|
||||
|
||||
switch (level) {
|
||||
case DEBUG:
|
||||
if (throwable != null) {
|
||||
Log.d(TAG, formattedMessage, throwable);
|
||||
} else {
|
||||
Log.d(TAG, formattedMessage);
|
||||
}
|
||||
break;
|
||||
|
||||
case INFO:
|
||||
if (throwable != null) {
|
||||
Log.i(TAG, formattedMessage, throwable);
|
||||
} else {
|
||||
Log.i(TAG, formattedMessage);
|
||||
}
|
||||
break;
|
||||
|
||||
case WARNING:
|
||||
if (throwable != null) {
|
||||
Log.w(TAG, formattedMessage, throwable);
|
||||
} else {
|
||||
Log.w(TAG, formattedMessage);
|
||||
}
|
||||
break;
|
||||
|
||||
case ERROR:
|
||||
if (throwable != null) {
|
||||
Log.e(TAG, formattedMessage, throwable);
|
||||
} else {
|
||||
Log.e(TAG, formattedMessage);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private String formatMessage(String message) {
|
||||
if (message == null) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
return String.format("[%s] %s", TAG, message);
|
||||
}
|
||||
}
|
||||
@@ -1,736 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationPlugin.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Handles daily notification scheduling and management on Android
|
||||
*
|
||||
* Features:
|
||||
* - Daily notification scheduling with precise timing
|
||||
* - Notification channel management
|
||||
* - Settings persistence
|
||||
* - Background task handling
|
||||
* - Battery optimization support
|
||||
* - Error handling and logging
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
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.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.PowerManager;
|
||||
import android.os.SystemClock;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
@CapacitorPlugin(name = "DailyNotification")
|
||||
public class DailyNotificationPlugin extends Plugin {
|
||||
private static final String TAG = "DailyNotificationPlugin";
|
||||
private static final String CHANNEL_ID = "daily_notification_channel";
|
||||
private static final String CHANNEL_NAME = "Daily Notifications";
|
||||
private static final String CHANNEL_DESCRIPTION = "Daily notification updates";
|
||||
private static final String SETTINGS_KEY = "daily_notification_settings";
|
||||
private static final String BATTERY_STATS_KEY = "battery_stats";
|
||||
private static final String BATTERY_OPTIMIZATION_KEY = "battery_optimization_status";
|
||||
private static final String BATTERY_CHECK_INTERVAL = "battery_check_interval";
|
||||
private static final String LAST_NOTIFICATION_KEY = "last_notification_time";
|
||||
private static final String POWER_STATE_KEY = "power_state";
|
||||
private static final String ADAPTIVE_SCHEDULING_KEY = "adaptive_scheduling";
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
private AlarmManager alarmManager;
|
||||
private PowerManager powerManager;
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
private Context context;
|
||||
private SharedPreferences settings;
|
||||
private DailyNotificationLogger logger;
|
||||
private boolean isOptimizationExempt = false;
|
||||
private long lastBatteryCheck;
|
||||
private int batteryLevel;
|
||||
private boolean isCharging;
|
||||
private long lastOptimizationCheck;
|
||||
private boolean adaptiveSchedulingEnabled = true;
|
||||
private int powerState = PowerManager.POWER_STATE_NORMAL;
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
context = getContext();
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
settings = context.getSharedPreferences(SETTINGS_KEY, Context.MODE_PRIVATE);
|
||||
logger = new DailyNotificationLogger();
|
||||
|
||||
createNotificationChannel();
|
||||
initializeSettings();
|
||||
checkBatteryOptimization();
|
||||
initializeBatteryMonitoring();
|
||||
logger.log("Plugin loaded successfully", DailyNotificationLogger.Level.INFO);
|
||||
}
|
||||
|
||||
private void initializeSettings() {
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
if (!settings.contains("sound")) editor.putBoolean("sound", true);
|
||||
if (!settings.contains("priority")) editor.putString("priority", "default");
|
||||
if (!settings.contains("retryCount")) editor.putInt("retryCount", 3);
|
||||
if (!settings.contains("retryInterval")) editor.putInt("retryInterval", 1000);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
channel.setDescription(CHANNEL_DESCRIPTION);
|
||||
channel.enableVibration(true);
|
||||
channel.enableLights(true);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
logger.log("Notification channel created", DailyNotificationLogger.Level.INFO);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkBatteryOptimization() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
String packageName = context.getPackageName();
|
||||
isOptimizationExempt = powerManager.isIgnoringBatteryOptimizations(packageName);
|
||||
lastOptimizationCheck = System.currentTimeMillis();
|
||||
|
||||
// Store optimization status
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putBoolean(BATTERY_OPTIMIZATION_KEY, isOptimizationExempt);
|
||||
editor.putLong("last_optimization_check", lastOptimizationCheck);
|
||||
editor.apply();
|
||||
|
||||
logger.log("Battery optimization status: " + (isOptimizationExempt ? "exempt" : "not exempt"),
|
||||
DailyNotificationLogger.Level.INFO);
|
||||
|
||||
// If not exempt and haven't checked recently, request exemption
|
||||
if (!isOptimizationExempt &&
|
||||
System.currentTimeMillis() - lastOptimizationCheck > BATTERY_CHECK_INTERVAL) {
|
||||
requestBatteryOptimizationExemption(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void requestBatteryOptimizationExemption(PluginCall call) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Check if we already have exemption
|
||||
if (isOptimizationExempt) {
|
||||
logger.log("App already exempt from battery optimization",
|
||||
DailyNotificationLogger.Level.INFO);
|
||||
if (call != null) {
|
||||
call.resolve();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create intent for battery optimization settings
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(android.net.Uri.parse("package:" + context.getPackageName()));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
// Store current time for tracking
|
||||
long requestTime = System.currentTimeMillis();
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putLong("last_optimization_request", requestTime);
|
||||
editor.apply();
|
||||
|
||||
// Start the activity
|
||||
context.startActivity(intent);
|
||||
|
||||
// Schedule a check for the optimization status after a delay
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
checkBatteryOptimization();
|
||||
if (call != null) {
|
||||
JSObject result = new JSObject();
|
||||
result.put("isExempt", isOptimizationExempt);
|
||||
result.put("requestTime", requestTime);
|
||||
result.put("checkTime", System.currentTimeMillis());
|
||||
call.resolve(result);
|
||||
}
|
||||
}, 5000); // Check after 5 seconds
|
||||
|
||||
logger.log("Battery optimization exemption requested",
|
||||
DailyNotificationLogger.Level.INFO);
|
||||
} else {
|
||||
if (call != null) {
|
||||
call.reject("Battery optimization exemption not supported on this Android version");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.log("Error requesting battery optimization exemption: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
if (call != null) {
|
||||
call.reject("Failed to request battery optimization exemption: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void acquireWakeLock() {
|
||||
if (wakeLock == null) {
|
||||
wakeLock = powerManager.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"DailyNotification:WakeLock"
|
||||
);
|
||||
}
|
||||
|
||||
if (!wakeLock.isHeld()) {
|
||||
// Adjust wake lock duration based on battery level
|
||||
long duration = TimeUnit.MINUTES.toMillis(1);
|
||||
if (batteryLevel < 15) {
|
||||
duration = TimeUnit.SECONDS.toMillis(30);
|
||||
} else if (batteryLevel < 30) {
|
||||
duration = TimeUnit.SECONDS.toMillis(45);
|
||||
}
|
||||
|
||||
wakeLock.acquire(duration);
|
||||
logger.log("Wake lock acquired for " + duration + "ms", DailyNotificationLogger.Level.DEBUG);
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseWakeLock() {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
logger.log("Wake lock released", DailyNotificationLogger.Level.DEBUG);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDeviceInDozeMode() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return powerManager.isDeviceIdleMode();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleDozeMode() {
|
||||
if (isDeviceInDozeMode()) {
|
||||
logger.log("Device is in Doze mode", DailyNotificationLogger.Level.WARNING);
|
||||
|
||||
// If not exempt from battery optimization, try to request exemption
|
||||
if (!isOptimizationExempt) {
|
||||
requestBatteryOptimizationExemption(null);
|
||||
}
|
||||
|
||||
// Schedule a maintenance window for the next time the device exits Doze mode
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Use WorkManager for more reliable scheduling during Doze
|
||||
OneTimeWorkRequest maintenanceWork = new OneTimeWorkRequest.Builder(MaintenanceWorker.class)
|
||||
.setInitialDelay(15, TimeUnit.MINUTES)
|
||||
.setConstraints(new Constraints.Builder()
|
||||
.setRequiresDeviceIdle(false)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
WorkManager.getInstance(context).enqueue(maintenanceWork);
|
||||
|
||||
// Also set an alarm as backup
|
||||
alarmManager.setAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(15),
|
||||
createMaintenancePendingIntent()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PendingIntent createMaintenancePendingIntent() {
|
||||
Intent intent = new Intent(context, DailyNotificationReceiver.class);
|
||||
intent.setAction("MAINTENANCE_WINDOW");
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void scheduleDailyNotification(PluginCall call) {
|
||||
try {
|
||||
// Check battery optimization status
|
||||
if (!isOptimizationExempt) {
|
||||
logger.log("Warning: App is not exempt from battery optimization",
|
||||
DailyNotificationLogger.Level.WARNING);
|
||||
}
|
||||
|
||||
// Check Doze mode
|
||||
handleDozeMode();
|
||||
|
||||
// Acquire wake lock for scheduling
|
||||
acquireWakeLock();
|
||||
|
||||
String url = call.getString("url");
|
||||
String time = call.getString("time");
|
||||
|
||||
if (url == null || time == null) {
|
||||
throw new IllegalArgumentException("Missing required parameters");
|
||||
}
|
||||
|
||||
// Parse time string (HH:mm format)
|
||||
String[] timeComponents = time.split(":");
|
||||
if (timeComponents.length != 2) {
|
||||
throw new IllegalArgumentException("Invalid time format");
|
||||
}
|
||||
|
||||
int hour = Integer.parseInt(timeComponents[0]);
|
||||
int minute = Integer.parseInt(timeComponents[1]);
|
||||
|
||||
if (hour < 0 || hour >= 24 || minute < 0 || minute >= 60) {
|
||||
throw new IllegalArgumentException("Invalid time values");
|
||||
}
|
||||
|
||||
// Create calendar instance for the specified time
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(Calendar.HOUR_OF_DAY, hour);
|
||||
calendar.set(Calendar.MINUTE, minute);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
|
||||
// If the time has already passed today, schedule for tomorrow
|
||||
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
|
||||
calendar.add(Calendar.DAY_OF_YEAR, 1);
|
||||
}
|
||||
|
||||
// Create intent for the notification
|
||||
Intent intent = new Intent(context, DailyNotificationReceiver.class);
|
||||
intent.putExtra("url", url);
|
||||
intent.putExtra("title", call.getString("title", "Daily Notification"));
|
||||
intent.putExtra("body", call.getString("body", "Your daily update is ready"));
|
||||
intent.putExtra("sound", call.getBoolean("sound", settings.getBoolean("sound", true)));
|
||||
intent.putExtra("priority", call.getString("priority", settings.getString("priority", "default")));
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
url.hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
// Schedule the alarm with exact timing
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.getTimeInMillis(),
|
||||
pendingIntent
|
||||
);
|
||||
} else {
|
||||
throw new SecurityException("Cannot schedule exact alarms");
|
||||
}
|
||||
} else {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.getTimeInMillis(),
|
||||
pendingIntent
|
||||
);
|
||||
}
|
||||
|
||||
// Set repeating alarm for subsequent days
|
||||
alarmManager.setRepeating(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.getTimeInMillis() + TimeUnit.DAYS.toMillis(1),
|
||||
TimeUnit.DAYS.toMillis(1),
|
||||
pendingIntent
|
||||
);
|
||||
|
||||
// Release wake lock after scheduling
|
||||
releaseWakeLock();
|
||||
|
||||
// Store the scheduled time and URL for potential rescheduling
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putString("scheduled_time", time);
|
||||
editor.putString("notification_url", url);
|
||||
editor.putString("notification_title", call.getString("title", "Daily Notification"));
|
||||
editor.putString("notification_body", call.getString("body", "Your daily update is ready"));
|
||||
editor.apply();
|
||||
|
||||
logger.log("Notification scheduled for " + time, DailyNotificationLogger.Level.INFO);
|
||||
call.resolve();
|
||||
|
||||
} catch (Exception e) {
|
||||
releaseWakeLock();
|
||||
logger.log("Error scheduling notification: " + e.getMessage(), DailyNotificationLogger.Level.ERROR);
|
||||
call.reject("Failed to schedule notification: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void getLastNotification(PluginCall call) {
|
||||
try {
|
||||
// TODO: Implement last notification retrieval from local storage
|
||||
JSObject result = new JSObject();
|
||||
result.put("id", "");
|
||||
result.put("title", "");
|
||||
result.put("body", "");
|
||||
result.put("timestamp", 0);
|
||||
call.resolve(result);
|
||||
} catch (Exception e) {
|
||||
logger.log("Error getting last notification: " + e.getMessage(), DailyNotificationLogger.Level.ERROR);
|
||||
call.reject("Failed to get last notification: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void cancelAllNotifications(PluginCall call) {
|
||||
try {
|
||||
Intent intent = new Intent(context, DailyNotificationReceiver.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
alarmManager.cancel(pendingIntent);
|
||||
notificationManager.cancelAll();
|
||||
logger.log("All notifications cancelled", DailyNotificationLogger.Level.INFO);
|
||||
call.resolve();
|
||||
} catch (Exception e) {
|
||||
logger.log("Error cancelling notifications: " + e.getMessage(), DailyNotificationLogger.Level.ERROR);
|
||||
call.reject("Failed to cancel notifications: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void getNotificationStatus(PluginCall call) {
|
||||
try {
|
||||
JSObject result = new JSObject();
|
||||
result.put("nextNotificationTime", getNextNotificationTime());
|
||||
result.put("isEnabled", isNotificationEnabled());
|
||||
result.put("settings", getCurrentSettings());
|
||||
call.resolve(result);
|
||||
} catch (Exception e) {
|
||||
logger.log("Error getting notification status: " + e.getMessage(), DailyNotificationLogger.Level.ERROR);
|
||||
call.reject("Failed to get notification status: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void updateSettings(PluginCall call) {
|
||||
try {
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
|
||||
if (call.hasOption("sound")) {
|
||||
editor.putBoolean("sound", call.getBoolean("sound"));
|
||||
}
|
||||
|
||||
if (call.hasOption("priority")) {
|
||||
String priority = call.getString("priority");
|
||||
if (isValidPriority(priority)) {
|
||||
editor.putString("priority", priority);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid priority value");
|
||||
}
|
||||
}
|
||||
|
||||
if (call.hasOption("timezone")) {
|
||||
String timezone = call.getString("timezone");
|
||||
if (TimeZone.getTimeZone(timezone) != null) {
|
||||
editor.putString("timezone", timezone);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid timezone");
|
||||
}
|
||||
}
|
||||
|
||||
editor.apply();
|
||||
logger.log("Settings updated successfully", DailyNotificationLogger.Level.INFO);
|
||||
call.resolve(getCurrentSettings());
|
||||
} catch (Exception e) {
|
||||
logger.log("Error updating settings: " + e.getMessage(), DailyNotificationLogger.Level.ERROR);
|
||||
call.reject("Failed to update settings: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private long getNextNotificationTime() {
|
||||
// TODO: Implement next notification time retrieval
|
||||
return 0;
|
||||
}
|
||||
|
||||
private boolean isNotificationEnabled() {
|
||||
return notificationManager.areNotificationsEnabled();
|
||||
}
|
||||
|
||||
private JSObject getCurrentSettings() {
|
||||
JSObject settings = new JSObject();
|
||||
settings.put("sound", this.settings.getBoolean("sound", true));
|
||||
settings.put("priority", this.settings.getString("priority", "default"));
|
||||
settings.put("timezone", this.settings.getString("timezone", TimeZone.getDefault().getID()));
|
||||
return settings;
|
||||
}
|
||||
|
||||
private boolean isValidPriority(String priority) {
|
||||
return priority != null &&
|
||||
(priority.equals("high") ||
|
||||
priority.equals("default") ||
|
||||
priority.equals("low"));
|
||||
}
|
||||
|
||||
private void initializeBatteryMonitoring() {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
filter.addAction(Intent.ACTION_POWER_CONNECTED);
|
||||
filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
|
||||
filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
|
||||
context.registerReceiver(batteryReceiver, filter);
|
||||
updateBatteryStatus();
|
||||
updatePowerState();
|
||||
}
|
||||
|
||||
private final BroadcastReceiver batteryReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
updateBatteryStatus();
|
||||
}
|
||||
};
|
||||
|
||||
private void updateBatteryStatus() {
|
||||
Intent batteryStatus = context.registerReceiver(null,
|
||||
new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
|
||||
if (batteryStatus != null) {
|
||||
int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
|
||||
float batteryPct = level * 100 / (float)scale;
|
||||
|
||||
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
boolean charging = status == BatteryManager.BATTERY_HEALTH_CHARGING ||
|
||||
status == BatteryManager.BATTERY_HEALTH_FULL;
|
||||
|
||||
batteryLevel = (int)batteryPct;
|
||||
isCharging = charging;
|
||||
lastBatteryCheck = System.currentTimeMillis();
|
||||
|
||||
logger.log(String.format("Battery status updated: %d%% (%s)",
|
||||
batteryLevel,
|
||||
isCharging ? "charging" : "not charging"),
|
||||
DailyNotificationLogger.Level.DEBUG);
|
||||
|
||||
// Store battery stats
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putInt("last_battery_level", batteryLevel);
|
||||
editor.putBoolean("last_charging_state", isCharging);
|
||||
editor.putLong("last_battery_check", lastBatteryCheck);
|
||||
editor.apply();
|
||||
|
||||
// Adjust scheduling based on battery level if adaptive scheduling is enabled
|
||||
if (adaptiveSchedulingEnabled) {
|
||||
adjustSchedulingForBatteryLevel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustSchedulingForBatteryLevel() {
|
||||
if (batteryLevel < 15) {
|
||||
// Critical battery level - reduce frequency
|
||||
lastBatteryCheck = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(4);
|
||||
} else if (batteryLevel < 30) {
|
||||
// Low battery level - moderate frequency
|
||||
lastBatteryCheck = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(2);
|
||||
} else if (batteryLevel < 50) {
|
||||
// Medium battery level - standard frequency
|
||||
lastBatteryCheck = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePowerState() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
powerState = powerManager.getPowerState();
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putInt(POWER_STATE_KEY, powerState);
|
||||
editor.apply();
|
||||
|
||||
// Adjust scheduling based on power state
|
||||
if (adaptiveSchedulingEnabled) {
|
||||
adjustSchedulingForPowerState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustSchedulingForPowerState() {
|
||||
if (powerState == PowerManager.POWER_STATE_SAVE) {
|
||||
// In power save mode, increase the interval between checks
|
||||
lastBatteryCheck = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(2);
|
||||
} else if (powerState == PowerManager.POWER_STATE_NORMAL) {
|
||||
// In normal mode, use standard intervals
|
||||
lastBatteryCheck = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(30);
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void getBatteryStatus(PluginCall call) {
|
||||
try {
|
||||
// Check battery optimization status if needed
|
||||
if (System.currentTimeMillis() - lastOptimizationCheck > BATTERY_CHECK_INTERVAL) {
|
||||
checkBatteryOptimization();
|
||||
}
|
||||
|
||||
JSObject result = new JSObject();
|
||||
result.put("level", batteryLevel);
|
||||
result.put("isCharging", isCharging);
|
||||
result.put("lastCheck", lastBatteryCheck);
|
||||
result.put("isOptimizationExempt", isOptimizationExempt);
|
||||
result.put("lastOptimizationCheck", lastOptimizationCheck);
|
||||
result.put("isInDozeMode", isDeviceInDozeMode());
|
||||
call.resolve(result);
|
||||
} catch (Exception e) {
|
||||
logger.log("Error getting battery status: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
call.reject("Failed to get battery status: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleOnDestroy() {
|
||||
try {
|
||||
context.unregisterReceiver(batteryReceiver);
|
||||
} catch (Exception e) {
|
||||
logger.log("Error unregistering battery receiver: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
}
|
||||
super.handleOnDestroy();
|
||||
}
|
||||
|
||||
public void rescheduleMissedNotifications() {
|
||||
try {
|
||||
long lastNotificationTime = settings.getLong(LAST_NOTIFICATION_KEY, 0);
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
// If more than 24 hours have passed since the last notification
|
||||
if (currentTime - lastNotificationTime > TimeUnit.DAYS.toMillis(1)) {
|
||||
logger.log("Missed notifications detected, rescheduling", DailyNotificationLogger.Level.WARNING);
|
||||
|
||||
// Get the scheduled time from settings
|
||||
String scheduledTime = settings.getString("scheduled_time", null);
|
||||
if (scheduledTime != null) {
|
||||
// Parse the scheduled time
|
||||
String[] timeComponents = scheduledTime.split(":");
|
||||
if (timeComponents.length == 2) {
|
||||
int hour = Integer.parseInt(timeComponents[0]);
|
||||
int minute = Integer.parseInt(timeComponents[1]);
|
||||
|
||||
// Create calendar instance for the scheduled time
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(Calendar.HOUR_OF_DAY, hour);
|
||||
calendar.set(Calendar.MINUTE, minute);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
|
||||
// If the time has already passed today, schedule for tomorrow
|
||||
if (calendar.getTimeInMillis() <= currentTime) {
|
||||
calendar.add(Calendar.DAY_OF_YEAR, 1);
|
||||
}
|
||||
|
||||
// Get the notification URL from settings
|
||||
String url = settings.getString("notification_url", null);
|
||||
if (url != null) {
|
||||
// Create intent for the notification
|
||||
Intent intent = new Intent(context, DailyNotificationReceiver.class);
|
||||
intent.putExtra("url", url);
|
||||
intent.putExtra("title", settings.getString("notification_title", "Daily Notification"));
|
||||
intent.putExtra("body", settings.getString("notification_body", "Your daily update is ready"));
|
||||
intent.putExtra("sound", settings.getBoolean("sound", true));
|
||||
intent.putExtra("priority", settings.getString("priority", "default"));
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
url.hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
// Schedule the notification
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (alarmManager.canScheduleExactAlarms()) {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.getTimeInMillis(),
|
||||
pendingIntent
|
||||
);
|
||||
}
|
||||
} else {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
calendar.getTimeInMillis(),
|
||||
pendingIntent
|
||||
);
|
||||
}
|
||||
|
||||
logger.log("Missed notification rescheduled for " + scheduledTime,
|
||||
DailyNotificationLogger.Level.INFO);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.log("Error rescheduling missed notifications: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void setAdaptiveScheduling(PluginCall call) {
|
||||
try {
|
||||
boolean enabled = call.getBoolean("enabled", true);
|
||||
adaptiveSchedulingEnabled = enabled;
|
||||
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putBoolean(ADAPTIVE_SCHEDULING_KEY, enabled);
|
||||
editor.apply();
|
||||
|
||||
logger.log("Adaptive scheduling " + (enabled ? "enabled" : "disabled"),
|
||||
DailyNotificationLogger.Level.INFO);
|
||||
call.resolve();
|
||||
} catch (Exception e) {
|
||||
logger.log("Error setting adaptive scheduling: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
call.reject("Failed to set adaptive scheduling: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void getPowerState(PluginCall call) {
|
||||
try {
|
||||
JSObject result = new JSObject();
|
||||
result.put("powerState", powerState);
|
||||
result.put("adaptiveScheduling", adaptiveSchedulingEnabled);
|
||||
result.put("batteryLevel", batteryLevel);
|
||||
result.put("isCharging", isCharging);
|
||||
result.put("lastCheck", lastBatteryCheck);
|
||||
call.resolve(result);
|
||||
} catch (Exception e) {
|
||||
logger.log("Error getting power state: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
call.reject("Failed to get power state: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationReceiver.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Broadcast receiver for handling daily notifications
|
||||
*
|
||||
* Features:
|
||||
* - Notification display with custom actions
|
||||
* - Rich notification content
|
||||
* - Event broadcasting
|
||||
* - Error recovery
|
||||
* - Notification categories
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.app.Notification;
|
||||
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.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
public class DailyNotificationReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "DailyNotificationReceiver";
|
||||
private DailyNotificationLogger logger;
|
||||
|
||||
// Notification categories
|
||||
private static final String CATEGORY_DAILY = "DAILY_NOTIFICATION";
|
||||
|
||||
// Action identifiers
|
||||
private static final String ACTION_VIEW = "VIEW";
|
||||
private static final String ACTION_DISMISS = "DISMISS";
|
||||
private static final String ACTION_SNOOZE = "SNOOZE";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
logger = new DailyNotificationLogger();
|
||||
|
||||
try {
|
||||
String url = intent.getStringExtra("url");
|
||||
String title = intent.getStringExtra("title");
|
||||
String body = intent.getStringExtra("body");
|
||||
boolean sound = intent.getBooleanExtra("sound", true);
|
||||
String priority = intent.getStringExtra("priority");
|
||||
|
||||
if (url == null || title == null || body == null) {
|
||||
throw new IllegalArgumentException("Missing required notification parameters");
|
||||
}
|
||||
|
||||
showNotification(context, title, body, url, sound, priority);
|
||||
logger.log("Notification displayed successfully", DailyNotificationLogger.Level.INFO);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.log("Error displaying notification: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void showNotification(Context context, String title, String body,
|
||||
String url, boolean sound, String priority) {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
// Create notification channel if needed
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
DailyNotificationConstants.Channel.ID,
|
||||
DailyNotificationConstants.Channel.NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
channel.setDescription(DailyNotificationConstants.Channel.DESCRIPTION);
|
||||
channel.enableVibration(DailyNotificationConstants.Channel.ENABLE_VIBRATION);
|
||||
channel.enableLights(DailyNotificationConstants.Channel.ENABLE_LIGHTS);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context,
|
||||
DailyNotificationConstants.Channel.ID)
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setPriority(getNotificationPriority(priority))
|
||||
.setCategory(CATEGORY_DAILY)
|
||||
.setAutoCancel(true)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(body));
|
||||
|
||||
if (sound) {
|
||||
builder.setDefaults(NotificationCompat.DEFAULT_SOUND);
|
||||
}
|
||||
|
||||
// Create intent for notification tap
|
||||
Intent contentIntent = new Intent(context, context.getClass());
|
||||
contentIntent.setData(Uri.parse(url));
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
contentIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
builder.setContentIntent(pendingIntent);
|
||||
|
||||
// Add custom actions
|
||||
addCustomActions(builder, context, url);
|
||||
|
||||
Notification notification = builder.build();
|
||||
|
||||
// Use NotificationManagerCompat for better compatibility
|
||||
NotificationManagerCompat notificationManagerCompat =
|
||||
NotificationManagerCompat.from(context);
|
||||
|
||||
try {
|
||||
notificationManagerCompat.notify(url.hashCode(), notification);
|
||||
} catch (SecurityException e) {
|
||||
logger.log("Security exception while showing notification: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCustomActions(NotificationCompat.Builder builder, Context context, String url) {
|
||||
// View action
|
||||
Intent viewIntent = new Intent(context, context.getClass());
|
||||
viewIntent.setAction(ACTION_VIEW);
|
||||
viewIntent.setData(Uri.parse(url));
|
||||
|
||||
PendingIntent viewPendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
1,
|
||||
viewIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
builder.addAction(android.R.drawable.ic_menu_view, "View", viewPendingIntent);
|
||||
|
||||
// Snooze action
|
||||
Intent snoozeIntent = new Intent(context, context.getClass());
|
||||
snoozeIntent.setAction(ACTION_SNOOZE);
|
||||
snoozeIntent.putExtra("url", url);
|
||||
|
||||
PendingIntent snoozePendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
2,
|
||||
snoozeIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
builder.addAction(android.R.drawable.ic_menu_revert, "Snooze", snoozePendingIntent);
|
||||
|
||||
// Dismiss action
|
||||
Intent dismissIntent = new Intent(context, context.getClass());
|
||||
dismissIntent.setAction(ACTION_DISMISS);
|
||||
dismissIntent.putExtra("url", url);
|
||||
|
||||
PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
3,
|
||||
dismissIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
builder.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Dismiss", dismissPendingIntent);
|
||||
}
|
||||
|
||||
private int getNotificationPriority(String priority) {
|
||||
switch (priority.toLowerCase()) {
|
||||
case "high":
|
||||
return NotificationCompat.PRIORITY_HIGH;
|
||||
case "low":
|
||||
return NotificationCompat.PRIORITY_LOW;
|
||||
default:
|
||||
return NotificationCompat.PRIORITY_DEFAULT;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.app;
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
public class MaintenanceWorker extends Worker {
|
||||
private final DailyNotificationLogger logger;
|
||||
|
||||
public MaintenanceWorker(@NonNull Context context, @NonNull WorkerParameters params) {
|
||||
super(context, params);
|
||||
logger = new DailyNotificationLogger();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Result doWork() {
|
||||
try {
|
||||
logger.log("Maintenance work started", DailyNotificationLogger.Level.INFO);
|
||||
|
||||
// Perform maintenance tasks
|
||||
performMaintenance();
|
||||
|
||||
logger.log("Maintenance work completed successfully", DailyNotificationLogger.Level.INFO);
|
||||
return Result.success();
|
||||
} catch (Exception e) {
|
||||
logger.log("Error during maintenance work: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
return Result.failure();
|
||||
}
|
||||
}
|
||||
|
||||
private void performMaintenance() {
|
||||
// Check and update battery optimization status
|
||||
DailyNotificationPlugin plugin = DailyNotificationPlugin.getInstance();
|
||||
if (plugin != null) {
|
||||
plugin.checkBatteryOptimization();
|
||||
}
|
||||
|
||||
// Update battery status
|
||||
if (plugin != null) {
|
||||
plugin.updateBatteryStatus();
|
||||
}
|
||||
|
||||
// Reschedule any missed notifications
|
||||
if (plugin != null) {
|
||||
plugin.rescheduleMissedNotifications();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,423 +0,0 @@
|
||||
# DailyNotification Android Implementation
|
||||
|
||||
This directory contains the Android-specific implementation of the DailyNotification plugin for Capacitor.
|
||||
|
||||
## Overview
|
||||
|
||||
The DailyNotification plugin provides functionality for scheduling and managing daily notifications on Android devices. It uses Android's native notification system and background task scheduling to ensure reliable delivery of notifications.
|
||||
|
||||
## Features
|
||||
|
||||
- Daily notification scheduling with precise timing
|
||||
- Notification channel management (Android 8.0+)
|
||||
- Background task handling with WorkManager
|
||||
- Battery optimization support
|
||||
- Settings persistence
|
||||
- Rich logging capabilities
|
||||
- Comprehensive error handling
|
||||
- Extensive test coverage
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **DailyNotificationPlugin**
|
||||
- Main plugin class handling all notification operations
|
||||
- Manages notification scheduling and settings
|
||||
- Handles plugin method calls from JavaScript
|
||||
|
||||
2. **DailyNotificationReceiver**
|
||||
- Broadcast receiver for handling notification display
|
||||
- Manages notification content and presentation
|
||||
|
||||
3. **DailyNotificationLogger**
|
||||
- Structured logging system
|
||||
- Multiple log levels (DEBUG, INFO, WARNING, ERROR)
|
||||
- Configurable logging behavior
|
||||
|
||||
4. **DailyNotificationConstants**
|
||||
- Centralized constants management
|
||||
- Default values and configuration
|
||||
- Error messages and keys
|
||||
|
||||
5. **DailyNotificationConfig**
|
||||
- Configuration management
|
||||
- Settings persistence
|
||||
- Default values
|
||||
|
||||
### Key Features
|
||||
|
||||
#### Notification Scheduling
|
||||
- Uses AlarmManager for precise timing
|
||||
- Handles timezone changes
|
||||
- Supports multiple notifications per day
|
||||
- Battery-optimized scheduling
|
||||
|
||||
#### Background Processing
|
||||
- WorkManager for reliable background tasks
|
||||
- Battery optimization handling
|
||||
- Network state monitoring
|
||||
- Retry mechanism
|
||||
|
||||
#### Settings Management
|
||||
- Persistent storage using SharedPreferences
|
||||
- Default values management
|
||||
- Settings validation
|
||||
- Real-time updates
|
||||
|
||||
#### Error Handling
|
||||
- Comprehensive error catching
|
||||
- Detailed error messages
|
||||
- Logging integration
|
||||
- Recovery mechanisms
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Structure
|
||||
|
||||
1. **Unit Tests**
|
||||
- Core functionality testing
|
||||
- Settings management
|
||||
- Time validation
|
||||
- Error handling
|
||||
|
||||
2. **Integration Tests**
|
||||
- Notification scheduling
|
||||
- Background tasks
|
||||
- Settings persistence
|
||||
- Event handling
|
||||
|
||||
3. **Edge Cases**
|
||||
- Timezone changes
|
||||
- Battery optimization
|
||||
- Network issues
|
||||
- Resource cleanup
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
./gradlew test
|
||||
|
||||
# Run specific test class
|
||||
./gradlew test --tests "com.timesafari.dailynotification.DailyNotificationPluginTest"
|
||||
|
||||
# Run with coverage
|
||||
./gradlew test jacocoTestReport
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Data Storage**
|
||||
- Encrypted SharedPreferences for sensitive data
|
||||
- Secure notification content handling
|
||||
- Safe URL handling
|
||||
|
||||
2. **Permissions**
|
||||
- Runtime permission handling
|
||||
- Permission validation
|
||||
- Graceful degradation
|
||||
|
||||
3. **Background Tasks**
|
||||
- Battery optimization compliance
|
||||
- Resource usage monitoring
|
||||
- Task scheduling limits
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
1. **Battery Usage**
|
||||
- Efficient scheduling
|
||||
- Background task optimization
|
||||
- Wake lock management
|
||||
|
||||
2. **Memory Management**
|
||||
- Resource cleanup
|
||||
- Memory leak prevention
|
||||
- Cache management
|
||||
|
||||
3. **Network Usage**
|
||||
- Efficient data fetching
|
||||
- Caching strategies
|
||||
- Retry optimization
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Code Organization**
|
||||
- Clear package structure
|
||||
- Consistent naming conventions
|
||||
- Comprehensive documentation
|
||||
|
||||
2. **Error Handling**
|
||||
- Graceful degradation
|
||||
- User-friendly messages
|
||||
- Logging integration
|
||||
|
||||
3. **Testing**
|
||||
- Comprehensive test coverage
|
||||
- Edge case handling
|
||||
- Performance testing
|
||||
|
||||
## Dependencies
|
||||
|
||||
- AndroidX Core
|
||||
- AndroidX AppCompat
|
||||
- AndroidX WorkManager
|
||||
- AndroidX Room
|
||||
- AndroidX Lifecycle
|
||||
- AndroidX Security
|
||||
- JUnit
|
||||
- Mockito
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Commit your changes
|
||||
4. Push to the branch
|
||||
5. Create a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Author
|
||||
|
||||
Matthew Raymer
|
||||
|
||||
# Daily Notification Plugin - Battery Optimization Guide
|
||||
|
||||
## Battery Overview
|
||||
|
||||
The Daily Notification Plugin implements sophisticated battery optimization features to ensure reliable notification delivery while minimizing battery impact. This guide covers the implementation details and best practices for battery optimization.
|
||||
|
||||
## Battery Optimization Features
|
||||
|
||||
### 1. Adaptive Scheduling
|
||||
|
||||
- Adjusts notification timing based on battery level and power state
|
||||
- Reduces frequency during low battery conditions
|
||||
- Optimizes wake lock duration based on battery status
|
||||
- Implements smart rescheduling for missed notifications
|
||||
|
||||
### 2. Power State Management
|
||||
|
||||
- Monitors device power state (normal, power save, etc.)
|
||||
- Adapts notification behavior based on power mode
|
||||
- Implements Doze mode handling
|
||||
- Manages wake locks efficiently
|
||||
|
||||
### 3. Battery Level Monitoring
|
||||
|
||||
- Real-time battery level tracking
|
||||
- Charging state detection
|
||||
- Battery health monitoring
|
||||
- Adaptive thresholds based on battery status
|
||||
|
||||
## API Methods
|
||||
|
||||
### Battery Optimization
|
||||
|
||||
```typescript
|
||||
interface BatteryOptimizationStatus {
|
||||
isExempt: boolean;
|
||||
requestTime: number;
|
||||
checkTime: number;
|
||||
}
|
||||
|
||||
// Request battery optimization exemption
|
||||
requestBatteryOptimizationExemption(): Promise<BatteryOptimizationStatus>;
|
||||
|
||||
// Get current battery status
|
||||
getBatteryStatus(): Promise<{
|
||||
level: number;
|
||||
isCharging: boolean;
|
||||
lastCheck: number;
|
||||
isOptimizationExempt: boolean;
|
||||
lastOptimizationCheck: number;
|
||||
isInDozeMode: boolean;
|
||||
}>;
|
||||
|
||||
// Get power state information
|
||||
getPowerState(): Promise<{
|
||||
powerState: number;
|
||||
adaptiveScheduling: boolean;
|
||||
batteryLevel: number;
|
||||
isCharging: boolean;
|
||||
lastCheck: number;
|
||||
}>;
|
||||
|
||||
// Configure adaptive scheduling
|
||||
setAdaptiveScheduling(enabled: boolean): Promise<void>;
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Battery Optimization Exemption
|
||||
|
||||
```java
|
||||
@PluginMethod
|
||||
public void requestBatteryOptimizationExemption(PluginCall call) {
|
||||
// Implementation details...
|
||||
}
|
||||
```
|
||||
|
||||
### Power State Monitoring
|
||||
|
||||
```java
|
||||
private void updatePowerState() {
|
||||
// Implementation details...
|
||||
}
|
||||
```
|
||||
|
||||
### Adaptive Scheduling
|
||||
|
||||
```java
|
||||
private void adjustSchedulingForBatteryLevel() {
|
||||
// Implementation details...
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Battery Optimization Exemption
|
||||
- Request exemption only when necessary
|
||||
- Check exemption status before scheduling notifications
|
||||
- Implement fallback mechanisms when exemption is not granted
|
||||
|
||||
### 2. Wake Lock Usage
|
||||
- Use minimal wake lock duration
|
||||
- Release wake locks promptly
|
||||
- Implement battery-aware wake lock duration
|
||||
|
||||
### 3. Doze Mode Handling
|
||||
|
||||
- Schedule maintenance windows during Doze mode
|
||||
- Use WorkManager for reliable background tasks
|
||||
- Implement backup alarms for critical notifications
|
||||
|
||||
### 4. Battery Level Monitoring
|
||||
|
||||
- Monitor battery level changes
|
||||
- Adjust notification frequency based on battery status
|
||||
- Implement battery-saving thresholds
|
||||
|
||||
### 5. Power State Adaptation
|
||||
|
||||
- Adapt to power save mode
|
||||
- Optimize resource usage during low power
|
||||
- Implement graceful degradation
|
||||
|
||||
## Battery Error Handling
|
||||
|
||||
### Battery Optimization during Error Handling
|
||||
|
||||
```java
|
||||
try {
|
||||
// Battery optimization code
|
||||
} catch (Exception e) {
|
||||
logger.log("Error in battery optimization: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
// Handle error appropriately
|
||||
}
|
||||
```
|
||||
|
||||
### Wake Lock
|
||||
|
||||
```java
|
||||
try {
|
||||
// Wake lock code
|
||||
} catch (Exception e) {
|
||||
logger.log("Error managing wake lock: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
// Handle error appropriately
|
||||
}
|
||||
```
|
||||
|
||||
### Doze Mode
|
||||
|
||||
```java
|
||||
try {
|
||||
// Doze mode handling code
|
||||
} catch (Exception e) {
|
||||
logger.log("Error handling Doze mode: " + e.getMessage(),
|
||||
DailyNotificationLogger.Level.ERROR);
|
||||
// Handle error appropriately
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Battery Impact
|
||||
|
||||
- Minimize wake lock usage
|
||||
- Optimize background task scheduling
|
||||
- Implement efficient battery monitoring
|
||||
|
||||
### Resource Usage
|
||||
|
||||
- Use WorkManager for background tasks
|
||||
- Implement efficient wake lock management
|
||||
- Optimize notification scheduling
|
||||
|
||||
### Storage
|
||||
|
||||
- Efficient battery stats storage
|
||||
- Optimized settings persistence
|
||||
- Minimal logging overhead
|
||||
|
||||
## Security
|
||||
|
||||
### Wake Lock Security
|
||||
|
||||
- Use appropriate wake lock flags
|
||||
- Implement timeout mechanisms
|
||||
- Handle wake lock failures gracefully
|
||||
|
||||
### Data Security
|
||||
|
||||
- Secure storage of battery stats
|
||||
- Protected settings access
|
||||
- Safe logging practices
|
||||
|
||||
## Testing
|
||||
|
||||
### Battery Optimization Tests
|
||||
|
||||
```java
|
||||
@Test
|
||||
void testBatteryOptimizationExemptionRequest() {
|
||||
// Test implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Power State Tests
|
||||
|
||||
```java
|
||||
@Test
|
||||
void testPowerStateMonitoring() {
|
||||
// Test implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Tests
|
||||
```java
|
||||
@Test
|
||||
void testBatteryImpact() {
|
||||
// Test implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
### Documentation
|
||||
- Keep documentation up to date
|
||||
- Document all battery optimization features
|
||||
- Include usage examples
|
||||
|
||||
### Code
|
||||
- Follow battery optimization best practices
|
||||
- Implement comprehensive tests
|
||||
- Maintain backward compatibility
|
||||
|
||||
## License
|
||||
MIT License - See LICENSE file for details
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* Android implementation of the DailyNotification plugin
|
||||
* @module DailyNotificationAndroid
|
||||
*/
|
||||
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import type { DailyNotificationPlugin, DailyNotificationOptions, PermissionStatus } from '../definitions';
|
||||
|
||||
export class DailyNotificationAndroid implements DailyNotificationPlugin {
|
||||
private options: DailyNotificationOptions = {
|
||||
url: '',
|
||||
notificationTime: '09:00',
|
||||
title: 'Daily Update',
|
||||
body: 'Your daily notification is ready'
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize the daily notification system for Android
|
||||
* @param options Configuration options for the notification system
|
||||
*/
|
||||
async initialize(options: DailyNotificationOptions): Promise<void> {
|
||||
if (Capacitor.getPlatform() !== 'android') {
|
||||
throw new Error('This implementation is for Android only');
|
||||
}
|
||||
this.options = options;
|
||||
// TODO: Implement Android-specific initialization
|
||||
}
|
||||
|
||||
/**
|
||||
* Check current permission status for notifications
|
||||
* @returns Current permission status
|
||||
*/
|
||||
async checkPermissions(): Promise<PermissionStatus> {
|
||||
if (Capacitor.getPlatform() !== 'android') {
|
||||
throw new Error('This implementation is for Android only');
|
||||
}
|
||||
// TODO: Implement Android-specific permission check
|
||||
return { notifications: 'prompt' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permissions from the user
|
||||
* @returns Updated permission status after request
|
||||
*/
|
||||
async requestPermissions(): Promise<PermissionStatus> {
|
||||
if (Capacitor.getPlatform() !== 'android') {
|
||||
throw new Error('This implementation is for Android only');
|
||||
}
|
||||
// TODO: Implement Android-specific permission request
|
||||
return { notifications: 'prompt' };
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">capacitor-daily-notification</string>
|
||||
<string name="title_activity_main">capacitor-daily-notification</string>
|
||||
<string name="package_name">com.example.app</string>
|
||||
<string name="custom_url_scheme">com.example.app</string>
|
||||
<string name="app_name">DailyNotificationPlugin</string>
|
||||
<string name="title_activity_main">DailyNotificationPlugin</string>
|
||||
<string name="package_name">com.timesafari.dailynotification</string>
|
||||
<string name="custom_url_scheme">com.timesafari.dailynotification</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
/**
|
||||
* BatteryOptimizationSettingsTest.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Tests for battery optimization settings and power state management
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = {Build.VERSION_CODES.M})
|
||||
public class BatteryOptimizationSettingsTest {
|
||||
|
||||
@Mock
|
||||
private Context context;
|
||||
|
||||
@Mock
|
||||
private SharedPreferences settings;
|
||||
|
||||
@Mock
|
||||
private SharedPreferences.Editor editor;
|
||||
|
||||
@Mock
|
||||
private PowerManager powerManager;
|
||||
|
||||
@Mock
|
||||
private WorkManager workManager;
|
||||
|
||||
private BatteryOptimizationSettings batterySettings;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
|
||||
when(context.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager);
|
||||
when(settings.edit()).thenReturn(editor);
|
||||
when(editor.putBoolean(anyString(), anyBoolean())).thenReturn(editor);
|
||||
when(editor.putInt(anyString(), anyInt())).thenReturn(editor);
|
||||
when(editor.putLong(anyString(), anyLong())).thenReturn(editor);
|
||||
when(editor.apply()).thenReturn(true);
|
||||
|
||||
batterySettings = new BatteryOptimizationSettings(context, settings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestBatteryOptimizationExemption() {
|
||||
// Test when already exempt
|
||||
when(settings.getBoolean(anyString(), anyBoolean())).thenReturn(true);
|
||||
assertTrue(batterySettings.requestBatteryOptimizationExemption());
|
||||
|
||||
// Test when not exempt
|
||||
when(settings.getBoolean(anyString(), anyBoolean())).thenReturn(false);
|
||||
when(context.getPackageName()).thenReturn("com.test.app");
|
||||
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(android.net.Uri.parse("package:com.test.app"));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
batterySettings.requestBatteryOptimizationExemption();
|
||||
|
||||
verify(context).startActivity(eq(intent));
|
||||
verify(editor).putLong(eq("last_optimization_request"), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdatePowerState() {
|
||||
when(powerManager.getPowerState()).thenReturn(PowerManager.POWER_STATE_SAVE);
|
||||
|
||||
batterySettings.updatePowerState();
|
||||
|
||||
verify(editor).putInt(eq("power_state"), eq(PowerManager.POWER_STATE_SAVE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateBatteryStatus() {
|
||||
Intent batteryStatus = new Intent(Intent.ACTION_BATTERY_CHANGED);
|
||||
batteryStatus.putExtra(BatteryManager.EXTRA_LEVEL, 80);
|
||||
batteryStatus.putExtra(BatteryManager.EXTRA_SCALE, 100);
|
||||
batteryStatus.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_HEALTH_CHARGING);
|
||||
|
||||
when(context.registerReceiver(any(), any())).thenReturn(batteryStatus);
|
||||
|
||||
batterySettings.updateBatteryStatus();
|
||||
|
||||
assertEquals(80, batterySettings.getBatteryLevel());
|
||||
assertTrue(batterySettings.isCharging());
|
||||
verify(editor).putInt(eq("last_battery_level"), eq(80));
|
||||
verify(editor).putBoolean(eq("last_charging_state"), eq(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdjustSchedulingForPowerState() {
|
||||
// Test power save mode
|
||||
when(powerManager.getPowerState()).thenReturn(PowerManager.POWER_STATE_SAVE);
|
||||
batterySettings.updatePowerState();
|
||||
assertTrue(batterySettings.shouldCheckBattery());
|
||||
|
||||
// Test normal mode
|
||||
when(powerManager.getPowerState()).thenReturn(PowerManager.POWER_STATE_NORMAL);
|
||||
batterySettings.updatePowerState();
|
||||
assertTrue(batterySettings.shouldCheckBattery());
|
||||
|
||||
// Test optimized mode
|
||||
when(powerManager.getPowerState()).thenReturn(PowerManager.POWER_STATE_OPTIMIZED);
|
||||
batterySettings.updatePowerState();
|
||||
assertTrue(batterySettings.shouldCheckBattery());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPowerStateString() {
|
||||
assertEquals("POWER_SAVE", batterySettings.getPowerStateString(PowerManager.POWER_STATE_SAVE));
|
||||
assertEquals("NORMAL", batterySettings.getPowerStateString(PowerManager.POWER_STATE_NORMAL));
|
||||
assertEquals("OPTIMIZED", batterySettings.getPowerStateString(PowerManager.POWER_STATE_OPTIMIZED));
|
||||
assertEquals("UNKNOWN", batterySettings.getPowerStateString(999));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldCheckBattery() {
|
||||
// Test when check is needed
|
||||
assertTrue(batterySettings.shouldCheckBattery());
|
||||
|
||||
// Test when check is not needed
|
||||
when(settings.getLong(anyString(), anyLong())).thenReturn(System.currentTimeMillis());
|
||||
assertFalse(batterySettings.shouldCheckBattery());
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationLoggerTest.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Tests for logging functionality
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class DailyNotificationLoggerTest {
|
||||
|
||||
private DailyNotificationLogger logger;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
logger = new DailyNotificationLogger();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogWithInfoLevel() {
|
||||
// Test info level logging
|
||||
logger.log("Test info message", DailyNotificationLogger.Level.INFO);
|
||||
|
||||
// Verify log level
|
||||
assertEquals(DailyNotificationLogger.Level.INFO, logger.getLastLogLevel());
|
||||
|
||||
// Verify message
|
||||
assertTrue(logger.getLastLogMessage().contains("Test info message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogWithWarningLevel() {
|
||||
// Test warning level logging
|
||||
logger.log("Test warning message", DailyNotificationLogger.Level.WARNING);
|
||||
|
||||
// Verify log level
|
||||
assertEquals(DailyNotificationLogger.Level.WARNING, logger.getLastLogLevel());
|
||||
|
||||
// Verify message
|
||||
assertTrue(logger.getLastLogMessage().contains("Test warning message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogWithErrorLevel() {
|
||||
// Test error level logging
|
||||
logger.log("Test error message", DailyNotificationLogger.Level.ERROR);
|
||||
|
||||
// Verify log level
|
||||
assertEquals(DailyNotificationLogger.Level.ERROR, logger.getLastLogLevel());
|
||||
|
||||
// Verify message
|
||||
assertTrue(logger.getLastLogMessage().contains("Test error message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogWithDebugLevel() {
|
||||
// Test debug level logging
|
||||
logger.log("Test debug message", DailyNotificationLogger.Level.DEBUG);
|
||||
|
||||
// Verify log level
|
||||
assertEquals(DailyNotificationLogger.Level.DEBUG, logger.getLastLogLevel());
|
||||
|
||||
// Verify message
|
||||
assertTrue(logger.getLastLogMessage().contains("Test debug message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogWithException() {
|
||||
// Create test exception
|
||||
Exception testException = new RuntimeException("Test exception");
|
||||
|
||||
// Test logging with exception
|
||||
logger.log("Test error with exception", DailyNotificationLogger.Level.ERROR, testException);
|
||||
|
||||
// Verify log level
|
||||
assertEquals(DailyNotificationLogger.Level.ERROR, logger.getLastLogLevel());
|
||||
|
||||
// Verify message contains exception details
|
||||
assertTrue(logger.getLastLogMessage().contains("Test error with exception"));
|
||||
assertTrue(logger.getLastLogMessage().contains("Test exception"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogWithNullMessage() {
|
||||
// Test logging with null message
|
||||
logger.log(null, DailyNotificationLogger.Level.INFO);
|
||||
|
||||
// Verify log level
|
||||
assertEquals(DailyNotificationLogger.Level.INFO, logger.getLastLogLevel());
|
||||
|
||||
// Verify message is empty
|
||||
assertTrue(logger.getLastLogMessage().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogWithNullLevel() {
|
||||
// Test logging with null level
|
||||
logger.log("Test message", null);
|
||||
|
||||
// Verify default level is INFO
|
||||
assertEquals(DailyNotificationLogger.Level.INFO, logger.getLastLogLevel());
|
||||
|
||||
// Verify message
|
||||
assertTrue(logger.getLastLogMessage().contains("Test message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogWithTimestamp() {
|
||||
// Test logging with timestamp
|
||||
logger.log("Test message with timestamp", DailyNotificationLogger.Level.INFO);
|
||||
|
||||
// Verify message contains timestamp
|
||||
assertTrue(logger.getLastLogMessage().matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogWithTag() {
|
||||
// Test logging with tag
|
||||
logger.log("Test message with tag", DailyNotificationLogger.Level.INFO, "TestTag");
|
||||
|
||||
// Verify message contains tag
|
||||
assertTrue(logger.getLastLogMessage().contains("[TestTag]"));
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationPluginTest.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Tests for the main plugin functionality
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.PluginCall;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = {Build.VERSION_CODES.M})
|
||||
public class DailyNotificationPluginTest {
|
||||
|
||||
@Mock
|
||||
private Context context;
|
||||
|
||||
@Mock
|
||||
private SharedPreferences settings;
|
||||
|
||||
@Mock
|
||||
private SharedPreferences.Editor editor;
|
||||
|
||||
@Mock
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
@Mock
|
||||
private AlarmManager alarmManager;
|
||||
|
||||
@Mock
|
||||
private PowerManager powerManager;
|
||||
|
||||
@Mock
|
||||
private PluginCall pluginCall;
|
||||
|
||||
private DailyNotificationPlugin plugin;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
|
||||
when(context.getSystemService(Context.NOTIFICATION_SERVICE)).thenReturn(notificationManager);
|
||||
when(context.getSystemService(Context.ALARM_SERVICE)).thenReturn(alarmManager);
|
||||
when(context.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager);
|
||||
when(settings.edit()).thenReturn(editor);
|
||||
when(editor.putBoolean(anyString(), anyBoolean())).thenReturn(editor);
|
||||
when(editor.putString(anyString(), anyString())).thenReturn(editor);
|
||||
when(editor.putLong(anyString(), anyLong())).thenReturn(editor);
|
||||
when(editor.apply()).thenReturn(true);
|
||||
|
||||
plugin = new DailyNotificationPlugin();
|
||||
plugin.load();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScheduleDailyNotification() {
|
||||
// Setup test data
|
||||
when(pluginCall.getString("url")).thenReturn("https://example.com");
|
||||
when(pluginCall.getString("time")).thenReturn("09:00");
|
||||
when(pluginCall.getString("title")).thenReturn("Test Notification");
|
||||
when(pluginCall.getString("body")).thenReturn("Test Body");
|
||||
when(pluginCall.getBoolean("sound", true)).thenReturn(true);
|
||||
when(pluginCall.getString("priority", "default")).thenReturn("default");
|
||||
|
||||
// Execute
|
||||
plugin.scheduleDailyNotification(pluginCall);
|
||||
|
||||
// Verify
|
||||
verify(alarmManager).setExactAndAllowWhileIdle(
|
||||
eq(AlarmManager.RTC_WAKEUP),
|
||||
anyLong(),
|
||||
any()
|
||||
);
|
||||
verify(editor).putString(eq("scheduled_time"), eq("09:00"));
|
||||
verify(editor).putString(eq("notification_url"), eq("https://example.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelAllNotifications() {
|
||||
// Execute
|
||||
plugin.cancelAllNotifications(pluginCall);
|
||||
|
||||
// Verify
|
||||
verify(alarmManager).cancel(any());
|
||||
verify(notificationManager).cancelAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNotificationStatus() {
|
||||
// Setup
|
||||
when(notificationManager.areNotificationsEnabled()).thenReturn(true);
|
||||
|
||||
// Execute
|
||||
plugin.getNotificationStatus(pluginCall);
|
||||
|
||||
// Verify
|
||||
verify(pluginCall).resolve(any(JSObject.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSettings() {
|
||||
// Setup
|
||||
when(pluginCall.hasOption("sound")).thenReturn(true);
|
||||
when(pluginCall.getBoolean("sound")).thenReturn(false);
|
||||
when(pluginCall.hasOption("priority")).thenReturn(true);
|
||||
when(pluginCall.getString("priority")).thenReturn("high");
|
||||
|
||||
// Execute
|
||||
plugin.updateSettings(pluginCall);
|
||||
|
||||
// Verify
|
||||
verify(editor).putBoolean("sound", false);
|
||||
verify(editor).putString("priority", "high");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestBatteryOptimizationExemption() {
|
||||
// Execute
|
||||
plugin.requestBatteryOptimizationExemption(pluginCall);
|
||||
|
||||
// Verify
|
||||
verify(context).startActivity(any(Intent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBatteryStatus() {
|
||||
// Execute
|
||||
plugin.getBatteryStatus(pluginCall);
|
||||
|
||||
// Verify
|
||||
verify(pluginCall).resolve(any(JSObject.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetAdaptiveScheduling() {
|
||||
// Setup
|
||||
when(pluginCall.getBoolean("enabled", true)).thenReturn(true);
|
||||
|
||||
// Execute
|
||||
plugin.setAdaptiveScheduling(pluginCall);
|
||||
|
||||
// Verify
|
||||
verify(editor).putBoolean(eq("adaptive_scheduling"), eq(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPowerState() {
|
||||
// Execute
|
||||
plugin.getPowerState(pluginCall);
|
||||
|
||||
// Verify
|
||||
verify(pluginCall).resolve(any(JSObject.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleDozeMode() {
|
||||
// Setup
|
||||
when(powerManager.isDeviceIdleMode()).thenReturn(true);
|
||||
|
||||
// Execute
|
||||
plugin.handleDozeMode();
|
||||
|
||||
// Verify
|
||||
verify(alarmManager).setAndAllowWhileIdle(
|
||||
eq(AlarmManager.RTC_WAKEUP),
|
||||
anyLong(),
|
||||
any()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* DailyNotificationReceiverTest.java
|
||||
* Daily Notification Plugin for Capacitor
|
||||
*
|
||||
* Tests for notification receiver functionality
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
package com.timesafari.dailynotification;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = {Build.VERSION_CODES.M})
|
||||
public class DailyNotificationReceiverTest {
|
||||
|
||||
@Mock
|
||||
private Context context;
|
||||
|
||||
@Mock
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
@Mock
|
||||
private NotificationManagerCompat notificationManagerCompat;
|
||||
|
||||
private DailyNotificationReceiver receiver;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
|
||||
when(context.getSystemService(Context.NOTIFICATION_SERVICE))
|
||||
.thenReturn(notificationManager);
|
||||
when(NotificationManagerCompat.from(context))
|
||||
.thenReturn(notificationManagerCompat);
|
||||
|
||||
receiver = new DailyNotificationReceiver();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnReceive() {
|
||||
// Setup test data
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("url", "https://example.com");
|
||||
intent.putExtra("title", "Test Notification");
|
||||
intent.putExtra("body", "Test Body");
|
||||
intent.putExtra("sound", true);
|
||||
intent.putExtra("priority", "high");
|
||||
|
||||
// Execute
|
||||
receiver.onReceive(context, intent);
|
||||
|
||||
// Verify
|
||||
verify(notificationManagerCompat).notify(
|
||||
eq("https://example.com".hashCode()),
|
||||
any(Notification.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnReceiveWithMissingParameters() {
|
||||
// Setup test data
|
||||
Intent intent = new Intent();
|
||||
|
||||
// Execute
|
||||
receiver.onReceive(context, intent);
|
||||
|
||||
// Verify
|
||||
verify(notificationManagerCompat, never()).notify(anyInt(), any(Notification.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShowNotification() {
|
||||
// Setup test data
|
||||
String title = "Test Notification";
|
||||
String body = "Test Body";
|
||||
String url = "https://example.com";
|
||||
boolean sound = true;
|
||||
String priority = "high";
|
||||
|
||||
// Execute
|
||||
receiver.showNotification(context, title, body, url, sound, priority);
|
||||
|
||||
// Verify
|
||||
verify(notificationManagerCompat).notify(
|
||||
eq(url.hashCode()),
|
||||
any(Notification.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddCustomActions() {
|
||||
// Setup test data
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "test_channel");
|
||||
String url = "https://example.com";
|
||||
|
||||
// Execute
|
||||
receiver.addCustomActions(builder, context, url);
|
||||
|
||||
// Verify
|
||||
verify(context, times(3)).getClass();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNotificationPriority() {
|
||||
// Test high priority
|
||||
assertEquals(
|
||||
NotificationCompat.PRIORITY_HIGH,
|
||||
receiver.getNotificationPriority("high")
|
||||
);
|
||||
|
||||
// Test low priority
|
||||
assertEquals(
|
||||
NotificationCompat.PRIORITY_LOW,
|
||||
receiver.getNotificationPriority("low")
|
||||
);
|
||||
|
||||
// Test default priority
|
||||
assertEquals(
|
||||
NotificationCompat.PRIORITY_DEFAULT,
|
||||
receiver.getNotificationPriority("default")
|
||||
);
|
||||
|
||||
// Test unknown priority
|
||||
assertEquals(
|
||||
NotificationCompat.PRIORITY_DEFAULT,
|
||||
receiver.getNotificationPriority("unknown")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Execute
|
||||
receiver.createNotificationChannel(context);
|
||||
|
||||
// Verify
|
||||
verify(notificationManager).createNotificationChannel(any(NotificationChannel.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user