diff --git a/.idea/.name b/.idea/.name
index 2cfa884..67f3ec4 100644
--- a/.idea/.name
+++ b/.idea/.name
@@ -1 +1 @@
-TriggerX Example
\ No newline at end of file
+TriggerX-Example
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
new file mode 100644
index 0000000..d7c7ce8
--- /dev/null
+++ b/.idea/dictionaries/project.xml
@@ -0,0 +1,8 @@
+
+
+
+ triggerx
+ triggrx
+
+
+
\ No newline at end of file
diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml
new file mode 100644
index 0000000..02b915b
--- /dev/null
+++ b/.idea/git_toolbox_prj.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 97f0a8e..eb7d3de 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -10,6 +10,7 @@
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6799f4f..8d04c95 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -2,6 +2,8 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.hilt)
+ alias(libs.plugins.ksp)
}
android {
@@ -40,20 +42,35 @@ android {
}
dependencies {
-
+ // Core libraries
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
- implementation(libs.androidx.activity.compose)
+
+ // Jetpack Compose
implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.activity.compose)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
+
+ // Hilt dependencies
+ implementation(libs.hilt.android)
+ implementation(libs.hilt.navigation.compose)
+ ksp(libs.hilt.compiler)
+
+ implementation(projects.triggerx)
+
+ // Unit testing
testImplementation(libs.junit)
+
+ // Android UI testing
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
+
+ // Debug-only tools
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 47b35be..16011ad 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,7 +2,16 @@
+
+
+
+
+
+
+
+
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/meticha/triggerxexample/MainActivity.kt b/app/src/main/java/com/meticha/triggerxexample/MainActivity.kt
index 3a86c68..21498ee 100644
--- a/app/src/main/java/com/meticha/triggerxexample/MainActivity.kt
+++ b/app/src/main/java/com/meticha/triggerxexample/MainActivity.kt
@@ -4,44 +4,23 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
+import com.meticha.triggerx.dsl.TriggerX
+import com.meticha.triggerxexample.alarm.AppAlarmActivity
+import com.meticha.triggerxexample.home.HomeScreen
import com.meticha.triggerxexample.ui.theme.TriggerXExampleTheme
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TriggerXExampleTheme {
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding)
- )
- }
+ HomeScreen()
}
}
}
}
-@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
- modifier = modifier
- )
-}
-
-@Preview(showBackground = true)
-@Composable
-fun GreetingPreview() {
- TriggerXExampleTheme {
- Greeting("Android")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/meticha/triggerxexample/TriggerXApplication.kt b/app/src/main/java/com/meticha/triggerxexample/TriggerXApplication.kt
new file mode 100644
index 0000000..07f3ac4
--- /dev/null
+++ b/app/src/main/java/com/meticha/triggerxexample/TriggerXApplication.kt
@@ -0,0 +1,21 @@
+package com.meticha.triggerxexample
+
+import android.app.Application
+import com.meticha.triggerx.dsl.TriggerX
+import com.meticha.triggerxexample.alarm.AppAlarmActivity
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class TriggerXApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ TriggerX.init(applicationContext) {
+ useDefaultNotification(
+ title = "My App Alarm",
+ message = "Your alarm is ringing!",
+ channelName = "My Awesome App Notifications"
+ )
+ activityClass = AppAlarmActivity::class.java
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/meticha/triggerxexample/alarm/AppAlarmActivity.kt b/app/src/main/java/com/meticha/triggerxexample/alarm/AppAlarmActivity.kt
new file mode 100644
index 0000000..22446b6
--- /dev/null
+++ b/app/src/main/java/com/meticha/triggerxexample/alarm/AppAlarmActivity.kt
@@ -0,0 +1,27 @@
+package com.meticha.triggerxexample.alarm
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.sp
+import com.meticha.triggerx.TriggerXActivity
+
+class AppAlarmActivity : TriggerXActivity() {
+ @Composable
+ override fun AlarmContent() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "CAVIN!",
+ fontSize = 50.sp,
+ color = Color.Red
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/meticha/triggerxexample/home/HomeScreen.kt b/app/src/main/java/com/meticha/triggerxexample/home/HomeScreen.kt
new file mode 100644
index 0000000..ec3d4cf
--- /dev/null
+++ b/app/src/main/java/com/meticha/triggerxexample/home/HomeScreen.kt
@@ -0,0 +1,50 @@
+package com.meticha.triggerxexample.home
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ElevatedButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.meticha.triggerx.permission.rememberAppPermissionState
+
+@Composable
+fun HomeScreen(
+ viewModel: HomeViewModel = hiltViewModel()
+) {
+ val context = LocalContext.current
+ val permissionState = rememberAppPermissionState()
+
+
+ Scaffold { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ ElevatedButton(
+ onClick = {
+ if (permissionState.allRequiredGranted()) {
+ viewModel.scheduleOneMinuteAlarm(
+ context
+ )
+ } else {
+ permissionState.requestPermission()
+ }
+ }
+ ) {
+ Text("Ane click karo have")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/meticha/triggerxexample/home/HomeViewModel.kt b/app/src/main/java/com/meticha/triggerxexample/home/HomeViewModel.kt
new file mode 100644
index 0000000..966c4f7
--- /dev/null
+++ b/app/src/main/java/com/meticha/triggerxexample/home/HomeViewModel.kt
@@ -0,0 +1,32 @@
+package com.meticha.triggerxexample.home
+
+import android.content.Context
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import com.meticha.triggerx.TriggerXAlarmScheduler
+import dagger.hilt.android.lifecycle.HiltViewModel
+import java.util.Calendar
+import javax.inject.Inject
+
+@HiltViewModel
+class HomeViewModel @Inject constructor() : ViewModel() {
+
+ companion object {
+ private const val TAG = "HomeViewModel"
+ }
+
+ private val appAlarmManager: TriggerXAlarmScheduler = TriggerXAlarmScheduler()
+
+ fun scheduleOneMinuteAlarm(context: Context): Boolean {
+ val triggerTime = Calendar.getInstance().apply {
+ add(Calendar.MINUTE, 1)
+ }.timeInMillis
+
+ Log.i(TAG, "Attempting to schedule alarm for 1 minute from now ($triggerTime)")
+ return appAlarmManager.scheduleAlarm(context, triggerTime)
+ }
+
+ fun cancelCurrentAlarm(context: Context, id: Int) {
+ appAlarmManager.cancelAlarm(context, id)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 952b930..16c355d 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,4 +3,7 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.hilt) apply false
+ alias(libs.plugins.ksp) apply false
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0337a9d..8a67cc7 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,7 @@
[versions]
agp = "8.12.0-alpha03"
-kotlin = "2.0.21"
+datastorePreferences = "1.1.7"
+kotlin = "2.1.20"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
@@ -8,9 +9,16 @@ espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.9.0"
activityCompose = "1.10.1"
composeBom = "2024.09.00"
+appcompat = "1.6.1"
+material = "1.10.0"
+kotlinSerialization = "2.1.21"
+hiltNavigation = "1.2.0"
+hilt = "2.56.2"
+ksp = "2.1.20-2.0.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -24,9 +32,19 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+
+hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
+hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
+hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigation" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
+hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 0ff83eb..674f6a0 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -18,7 +18,8 @@ dependencyResolutionManagement {
mavenCentral()
}
}
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
-rootProject.name = "TriggerX Example"
+rootProject.name = "TriggerX-Example"
include(":app")
-
\ No newline at end of file
+include(":triggerx")
diff --git a/triggerx/.gitignore b/triggerx/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/triggerx/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/triggerx/build.gradle.kts b/triggerx/build.gradle.kts
new file mode 100644
index 0000000..f10abcb
--- /dev/null
+++ b/triggerx/build.gradle.kts
@@ -0,0 +1,67 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.hilt)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.jetbrains.kotlin.serialization)
+}
+
+android {
+ namespace = "com.meticha.triggerx"
+ compileSdk = 36
+
+ defaultConfig {
+ minSdk = 26
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+
+ // hilt
+ implementation(libs.hilt.android)
+ implementation(libs.hilt.navigation.compose)
+ ksp(libs.hilt.compiler)
+
+ // datastore
+ implementation(libs.androidx.datastore.preferences)
+
+
+ // compose dependencies
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+}
\ No newline at end of file
diff --git a/triggerx/consumer-rules.pro b/triggerx/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/triggerx/proguard-rules.pro b/triggerx/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/triggerx/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/triggerx/src/androidTest/java/com/meticha/triggerx/ExampleInstrumentedTest.kt b/triggerx/src/androidTest/java/com/meticha/triggerx/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..90abd26
--- /dev/null
+++ b/triggerx/src/androidTest/java/com/meticha/triggerx/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.meticha.triggerx
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.meticha.triggerx.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/triggerx/src/main/AndroidManifest.xml b/triggerx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..68a159d
--- /dev/null
+++ b/triggerx/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/triggerx/src/main/java/com/meticha/triggerx/DefaultTriggerActivity.kt b/triggerx/src/main/java/com/meticha/triggerx/DefaultTriggerActivity.kt
new file mode 100644
index 0000000..97b0f8a
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/DefaultTriggerActivity.kt
@@ -0,0 +1,26 @@
+package com.meticha.triggerx
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.sp
+
+class DefaultTriggerActivity : TriggerXActivity() {
+ @Composable
+ override fun AlarmContent() {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "TRIGGERX!",
+ fontSize = 50.sp,
+ color = Color.Red
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/triggerx/src/main/java/com/meticha/triggerx/TriggerXActivity.kt b/triggerx/src/main/java/com/meticha/triggerx/TriggerXActivity.kt
new file mode 100644
index 0000000..a78a2fe
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/TriggerXActivity.kt
@@ -0,0 +1,38 @@
+package com.meticha.triggerx
+
+import android.os.Build
+import android.os.Bundle
+import android.view.WindowManager
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.Composable
+
+
+abstract class TriggerXActivity : ComponentActivity() {
+
+ /** Override this to paint your own UI. */
+ @Composable
+ abstract fun AlarmContent()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // ── Required window flags ──────────────────────────────────────────
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ setShowWhenLocked(true)
+ setTurnScreenOn(true)
+ } else {
+ @Suppress("DEPRECATION")
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
+ WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ )
+ }
+
+ // ── Render developer-supplied Compose UI ──────────────────────────
+ setContent { AlarmContent() }
+ }
+}
+
diff --git a/triggerx/src/main/java/com/meticha/triggerx/TriggerXAlarmScheduler.kt b/triggerx/src/main/java/com/meticha/triggerx/TriggerXAlarmScheduler.kt
new file mode 100644
index 0000000..3dda0e4
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/TriggerXAlarmScheduler.kt
@@ -0,0 +1,90 @@
+package com.meticha.triggerx
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.core.content.getSystemService
+import com.meticha.triggerx.logger.LoggerConfig
+import com.meticha.triggerx.receivers.TriggerXAlarmReceiver
+
+class TriggerXAlarmScheduler {
+ fun scheduleAlarm(
+ context: Context,
+ triggerAtMillis: Long,
+ alarmId: Int = (System.currentTimeMillis() % Int.MAX_VALUE).toInt()
+ ): Boolean {
+ val alarmManager = context.getSystemService()!!
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (!alarmManager.canScheduleExactAlarms()) {
+ LoggerConfig.logger.e("Cannot schedule exact alarms. Permission not granted.")
+ // Optionally, redirect to settings or notify user.
+ // For now, returning false as the HomeScreen should handle permission request.
+ return false
+ }
+ }
+
+ val intent = Intent(context, TriggerXAlarmReceiver::class.java).apply {
+ action = TriggerXAlarmReceiver.ALARM_ACTION
+ }
+ val pendingIntent = PendingIntent.getBroadcast(
+ /* context = */ context,
+ /* requestCode = */ alarmId,
+ /* intent = */ intent,
+ /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ try {
+ // Use setExactAndAllowWhileIdle for alarms that need to fire even in Doze mode.
+ // For AlarmClock behavior (shows in status bar), use AlarmManager.AlarmClockInfo
+ alarmManager.setExactAndAllowWhileIdle(
+ AlarmManager.RTC_WAKEUP,
+ triggerAtMillis,
+ pendingIntent
+ )
+ LoggerConfig.logger.i("Alarm [$alarmId] scheduled for $triggerAtMillis")
+ return true
+ } catch (se: SecurityException) {
+ LoggerConfig.logger.e(
+ "SecurityException while scheduling alarm. Check permissions.",
+ se
+ )
+ return false
+ } catch (e: Exception) {
+ LoggerConfig.logger.e("Exception while scheduling alarm", e)
+ return false
+ }
+ }
+
+ fun scheduleMultipleAlarms(
+ context: Context,
+ events: List> // Pair
+ ): List {
+ return events.map { (id, time) ->
+ scheduleAlarm(context, time, id)
+ }
+ }
+
+ fun cancelAlarm(context: Context, alarmId: Int) {
+ val intent = Intent(context, TriggerXAlarmReceiver::class.java).apply {
+ action = TriggerXAlarmReceiver.ALARM_ACTION
+ }
+ val pendingIntent = PendingIntent.getBroadcast(
+ context,
+ alarmId,
+ intent,
+ PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ if (pendingIntent != null) {
+ val alarmManager = context.getSystemService(AlarmManager::class.java)
+ alarmManager.cancel(pendingIntent)
+ pendingIntent.cancel()
+ LoggerConfig.logger.i("Alarm [$alarmId] cancelled.")
+ } else {
+ LoggerConfig.logger.i("Alarm [$alarmId] not found to cancel.")
+ }
+ }
+}
\ No newline at end of file
diff --git a/triggerx/src/main/java/com/meticha/triggerx/dsl/TriggerX.kt b/triggerx/src/main/java/com/meticha/triggerx/dsl/TriggerX.kt
new file mode 100644
index 0000000..faff469
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/dsl/TriggerX.kt
@@ -0,0 +1,47 @@
+package com.meticha.triggerx.dsl
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import com.meticha.triggerx.logger.DefaultTriggerXLogger
+import com.meticha.triggerx.logger.LoggerConfig
+import com.meticha.triggerx.preference.TriggerXPreferences
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+object TriggerX {
+ internal var config: TriggerXConfig? = null
+
+ fun init(context: Context, configure: TriggerXConfig.() -> Unit) {
+ val conf = TriggerXConfig().apply(configure)
+ config = conf
+
+ LoggerConfig.logger = conf.customLogger ?: DefaultTriggerXLogger
+ LoggerConfig.logger.d("TriggerX initialized with notification title: ${conf.notificationTitle}")
+
+ CoroutineScope(Dispatchers.IO).launch {
+ TriggerXPreferences.save(context.applicationContext, conf)
+ }
+
+ setupNotificationChannel(context.applicationContext)
+ }
+
+ private fun setupNotificationChannel(context: Context) {
+ val channelName = config?.notificationChannelName ?: "TriggerX Alarms"
+ val channel = NotificationChannel(
+ DEFAULT_CHANNEL_ID,
+ channelName,
+ NotificationManager.IMPORTANCE_DEFAULT
+ ).apply {
+ description = "Notifications for TriggerX alarms"
+ }
+ val nm = context.getSystemService(NotificationManager::class.java)
+ nm.createNotificationChannel(channel)
+ }
+
+ const val DEFAULT_CHANNEL_ID = "triggerx_channel"
+
+ fun getNotificationTitle(): String = config?.notificationTitle ?: "Alarm"
+ fun getNotificationMessage(): String = config?.notificationMessage ?: "Alarm is ringing"
+}
diff --git a/triggerx/src/main/java/com/meticha/triggerx/dsl/TriggerXConfig.kt b/triggerx/src/main/java/com/meticha/triggerx/dsl/TriggerXConfig.kt
new file mode 100644
index 0000000..024d5e9
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/dsl/TriggerXConfig.kt
@@ -0,0 +1,30 @@
+package com.meticha.triggerx.dsl
+
+import android.app.Activity
+import com.meticha.triggerx.DefaultTriggerActivity
+import com.meticha.triggerx.logger.TriggerXLogger
+import kotlin.jvm.java
+
+class TriggerXConfig internal constructor() {
+ internal var notificationTitle: String? = null
+ internal var notificationMessage: String? = null
+ internal var notificationChannelName: String = "TriggerX Alarms"
+
+ internal var customLogger: TriggerXLogger? = null
+ var activityClass: Class = DefaultTriggerActivity::class.java
+
+
+ fun useDefaultNotification(
+ title: String,
+ message: String,
+ channelName: String = "TriggerX Alarms"
+ ) {
+ notificationTitle = title
+ notificationMessage = message
+ notificationChannelName = channelName
+ }
+
+ fun logging(logger: TriggerXLogger) {
+ customLogger = logger
+ }
+}
diff --git a/triggerx/src/main/java/com/meticha/triggerx/logger/DefaultTriggerXLogger.kt b/triggerx/src/main/java/com/meticha/triggerx/logger/DefaultTriggerXLogger.kt
new file mode 100644
index 0000000..56cd37e
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/logger/DefaultTriggerXLogger.kt
@@ -0,0 +1,21 @@
+package com.meticha.triggerx.logger
+
+import android.util.Log
+
+object DefaultTriggerXLogger : TriggerXLogger {
+ override fun d(message: String) {
+ Log.d(LoggerConfig.TAG, message)
+ }
+
+ override fun e(message: String, throwable: Throwable?) {
+ Log.e(LoggerConfig.TAG, message, throwable)
+ }
+
+ override fun i(message: String) {
+ Log.i(LoggerConfig.TAG, message)
+ }
+
+ override fun w(message: String) {
+ Log.w(LoggerConfig.TAG, message)
+ }
+}
\ No newline at end of file
diff --git a/triggerx/src/main/java/com/meticha/triggerx/logger/TriggerXLogger.kt b/triggerx/src/main/java/com/meticha/triggerx/logger/TriggerXLogger.kt
new file mode 100644
index 0000000..eb4b0aa
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/logger/TriggerXLogger.kt
@@ -0,0 +1,8 @@
+package com.meticha.triggerx.logger
+
+interface TriggerXLogger {
+ fun d(message: String)
+ fun e(message: String, throwable: Throwable? = null)
+ fun i(message: String)
+ fun w(message: String)
+}
\ No newline at end of file
diff --git a/triggerx/src/main/java/com/meticha/triggerx/logger/TriggerXLoggerConfig.kt b/triggerx/src/main/java/com/meticha/triggerx/logger/TriggerXLoggerConfig.kt
new file mode 100644
index 0000000..6fef169
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/logger/TriggerXLoggerConfig.kt
@@ -0,0 +1,7 @@
+package com.meticha.triggerx.logger
+
+object LoggerConfig {
+ const val TAG = "TRIGGERX"
+
+ var logger: TriggerXLogger = DefaultTriggerXLogger
+}
\ No newline at end of file
diff --git a/triggerx/src/main/java/com/meticha/triggerx/permission/TriggerXPermissionComposable.kt b/triggerx/src/main/java/com/meticha/triggerx/permission/TriggerXPermissionComposable.kt
new file mode 100644
index 0000000..e5ce4bc
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/permission/TriggerXPermissionComposable.kt
@@ -0,0 +1,152 @@
+package com.meticha.triggerx.permission
+
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import com.meticha.triggerx.logger.LoggerConfig
+import com.meticha.triggerx.permission.AlarmPermissionManager.isGranted
+import java.lang.ref.WeakReference
+
+
+@SuppressLint("ComposableNaming")
+@Composable
+fun rememberAppPermissionState(): PermissionState {
+
+ val permissions = buildList {
+ addAll(
+ listOf(
+ PermissionType.ALARM,
+ PermissionType.OVERLAY,
+ PermissionType.BATTERY_OPTIMIZATION,
+ PermissionType.NOTIFICATION
+ )
+ )
+ if (Build.MANUFACTURER.equals("Xiaomi", true)) {
+ add(PermissionType.LOCK_SCREEN)
+ }
+ }
+
+ val context = LocalContext.current
+
+ val permissionState = remember(permissions) { PermissionState(permissions) }
+
+ // Provide context access through composable scope
+ permissionState.contextRef = WeakReference(context)
+
+
+ // Handle lifecycle events
+ PermissionLifeCycleCheckEffect(
+ permissionState = permissionState
+ )
+
+ // Set up permission launcher
+ permissionState.launcher = rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ when {
+ isGranted(context, permissionState.currentPermission!!) -> {
+ permissionState.allRequiredGranted()
+ permissionState.next()
+ }
+
+ else -> handlePermissionDenial(
+ permissionState
+ )
+ }
+ }
+
+ // Display permission rationale popup if needed
+ permissionState.currentPermission?.let { permission ->
+ when {
+ permissionState.showRationalePopUp -> {
+ ShowPopup(
+ message = "Permissions are required to proceed further",
+ onConfirm = {
+ permissionState.showRationalePopUp = false
+ permissionState.requestPermission()
+ },
+ onDismiss = {
+ permissionState.showRationalePopUp = false
+ }
+ )
+ }
+ }
+ }
+
+ return permissionState
+}
+
+
+/**
+ * Handles permission denial, showing appropriate UI based on denial context
+ */
+private fun handlePermissionDenial(permissionState: PermissionState) {
+ permissionState.showRationalePopUp = true
+}
+
+
+@Composable
+fun PermissionLifeCycleCheckEffect(
+ permissionState: PermissionState,
+ lifecycleEvent: Lifecycle.Event = Lifecycle.Event.ON_RESUME,
+) {
+
+ val observer = LifecycleEventObserver { _, event ->
+ LoggerConfig.logger.d("Event: $event")
+ if (event == lifecycleEvent) {
+ if (permissionState.resumedFromSettings) {
+ permissionState.requestPermission()
+ permissionState.resumedFromSettings = false
+ }
+ }
+ }
+ val lifecycleOwner = LocalLifecycleOwner.current
+ DisposableEffect(lifecycleOwner) {
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+}
+
+@Composable
+fun ShowPopup(
+ message: String,
+ onConfirm: () -> Unit,
+ onDismiss: () -> Unit,
+) {
+ AlertDialog(
+ onDismissRequest = { onDismiss() },
+ title = {
+ Text(text = "Permission")
+ },
+ text = {
+ Text(text = message)
+ },
+ dismissButton = {
+ Button(onClick = {
+ onDismiss()
+ }) {
+ Text(text = "Cancel")
+ }
+ },
+ confirmButton = {
+ Button(onClick = {
+ onConfirm()
+ }) {
+ Text(text = "Grant")
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/triggerx/src/main/java/com/meticha/triggerx/permission/TriggerXPermissionManager.kt b/triggerx/src/main/java/com/meticha/triggerx/permission/TriggerXPermissionManager.kt
new file mode 100644
index 0000000..6c84f21
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/permission/TriggerXPermissionManager.kt
@@ -0,0 +1,214 @@
+package com.meticha.triggerx.permission
+
+import android.annotation.SuppressLint
+import android.app.AlarmManager
+import android.app.AppOpsManager
+import android.content.Context
+import android.content.Context.APP_OPS_SERVICE
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Binder
+import android.os.Build
+import android.provider.Settings
+import androidx.activity.compose.ManagedActivityResultLauncher
+import androidx.activity.result.ActivityResult
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
+import java.lang.ref.WeakReference
+import java.lang.reflect.Method
+
+
+object AlarmPermissionManager {
+ fun hasExactAlarmPermission(context: Context): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ context.getSystemService(AlarmManager::class.java).canScheduleExactAlarms()
+ } else {
+ true // On older versions, this permission is implicitly granted
+ }
+ }
+
+ fun hasOverlayPermission(context: Context): Boolean {
+ return Settings.canDrawOverlays(context)
+ }
+
+ fun hasBatteryOptimizationPermission(context: Context): Boolean {
+ val powerManager =
+ context.getSystemService(Context.POWER_SERVICE) as android.os.PowerManager
+ val packageName = context.packageName
+ return powerManager.isIgnoringBatteryOptimizations(packageName)
+ }
+
+ @SuppressLint("DiscouragedPrivateApi")
+ // TODO("Need to check in future")
+ fun isShowOnLockScreenPermissionEnable(context: Context): Boolean {
+ return try {
+ val manager = context.getSystemService(APP_OPS_SERVICE) as AppOpsManager
+ val method: Method = AppOpsManager::class.java.getDeclaredMethod(
+ "checkOpNoThrow",
+ Int::class.javaPrimitiveType,
+ Int::class.javaPrimitiveType,
+ String::class.java
+ )
+ val result =
+ method.invoke(manager, 10020, Binder.getCallingUid(), context.packageName) as Int
+ AppOpsManager.MODE_ALLOWED == result
+ } catch (_: Exception) {
+ false
+ }
+ }
+
+ fun isNotificationPermissionEnabled(context: Context): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ContextCompat.checkSelfPermission(
+ context,
+ android.Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ } else {
+ true // Notifications don't need explicit permission pre-Android 13
+ }
+ }
+
+ fun isGranted(context: Context, permission: PermissionType): Boolean {
+ return when (permission) {
+ PermissionType.ALARM -> hasExactAlarmPermission(context)
+ PermissionType.OVERLAY -> hasOverlayPermission(context)
+ PermissionType.BATTERY_OPTIMIZATION -> hasBatteryOptimizationPermission(context)
+ PermissionType.LOCK_SCREEN -> isShowOnLockScreenPermissionEnable(context)
+ PermissionType.NOTIFICATION -> isNotificationPermissionEnabled(context)
+ }
+ }
+
+ fun createPermissionIntent(context: Context, permissionType: PermissionType): Intent {
+ when (permissionType) {
+ PermissionType.ALARM -> {
+ return Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
+ data = "package:${context.packageName}".toUri()
+ }
+ }
+
+ PermissionType.OVERLAY -> {
+ return Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
+ data = "package:${context.packageName}".toUri()
+ }
+ }
+
+ PermissionType.BATTERY_OPTIMIZATION -> {
+ return Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
+ data = "package:${context.packageName}".toUri()
+ }
+ }
+
+ PermissionType.LOCK_SCREEN -> {
+ return Intent("miui.intent.action.APP_PERM_EDITOR").apply {
+ setClassName(
+ "com.miui.securitycenter",
+ "com.miui.permcenter.permissions.PermissionsEditorActivity"
+ )
+ putExtra("extra_pkgname", context.packageName)
+ }
+ }
+
+ PermissionType.NOTIFICATION -> {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
+ putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
+ }
+ } else {
+ Intent()
+ }
+ }
+
+ }
+ }
+}
+
+enum class PermissionType { ALARM, OVERLAY, BATTERY_OPTIMIZATION, LOCK_SCREEN, NOTIFICATION }
+
+/**
+ * Manages the state of permission requests and their UI flows
+ */
+class PermissionState(
+ permissionList: List,
+) {
+ // WeakReference to context to avoid memory leaks
+ lateinit var contextRef: WeakReference
+
+ // All permissions that need to be handled
+ private val allPermissions = mutableStateListOf()
+
+ // Permissions waiting to be processed
+ private var pendingPermissions = mutableStateListOf()
+
+ // Currently processing permission
+ internal var currentPermission by mutableStateOf(null)
+
+ // UI state
+ internal var showRationalePopUp by mutableStateOf(false)
+ internal var resumedFromSettings by mutableStateOf(false)
+
+ // Permission states
+ private var isRequiredPermissionGranted by mutableStateOf(false)
+
+ // Permission request launcher
+ internal var launcher: ManagedActivityResultLauncher? = null
+
+ init {
+ allPermissions.addAll(permissionList)
+ pendingPermissions.addAll(permissionList)
+ }
+
+ /**
+ * Checks if all required permissions are actually granted
+ */
+ fun allRequiredGranted(): Boolean {
+ isRequiredPermissionGranted = allPermissions
+ .all { isGranted(it) }
+ return isRequiredPermissionGranted
+ }
+
+ /**
+ * Checks if a specific permission is granted
+ */
+ fun isGranted(permission: PermissionType): Boolean {
+ val context = requireNotNull(contextRef.get())
+ return AlarmPermissionManager.isGranted(context, permission)
+ }
+
+ /**
+ * Starts or continues the permission request flow
+ */
+ fun requestPermission() {
+ if (pendingPermissions.isNotEmpty()) {
+ currentPermission = pendingPermissions.first()
+ currentPermission?.let { permission ->
+ if (isGranted(permission)) {
+ next() // Permission already granted, move to next
+ } else {
+ launcher?.launch(
+ AlarmPermissionManager.createPermissionIntent(
+ requireNotNull(contextRef.get()),
+ permission
+ )
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Moves to the next permission in the queue
+ */
+ internal fun next() {
+ if (pendingPermissions.isNotEmpty()) {
+ pendingPermissions.removeAt(0)
+ }
+
+ requestPermission()
+ }
+
+}
+
diff --git a/triggerx/src/main/java/com/meticha/triggerx/preference/TriggerXPreference.kt b/triggerx/src/main/java/com/meticha/triggerx/preference/TriggerXPreference.kt
new file mode 100644
index 0000000..53e8568
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/preference/TriggerXPreference.kt
@@ -0,0 +1,51 @@
+package com.meticha.triggerx.preference
+
+import android.app.Activity
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import com.meticha.triggerx.dsl.TriggerXConfig
+import com.meticha.triggerx.logger.LoggerConfig
+import kotlinx.coroutines.flow.firstOrNull
+
+object TriggerXPreferences {
+ private val Context.dataStore: DataStore by preferencesDataStore(name = "triggerx_prefs")
+
+ private val KEY_ACTIVITY_CLASS = stringPreferencesKey("activity_class")
+ private val KEY_NOTIFICATION_TITLE = stringPreferencesKey("notification_title")
+ private val KEY_NOTIFICATION_MESSAGE = stringPreferencesKey("notification_message")
+
+ suspend fun save(context: Context, config: TriggerXConfig) {
+ context.dataStore.edit { prefs ->
+ prefs[KEY_ACTIVITY_CLASS] = config.activityClass.name
+ config.notificationTitle?.let {
+ prefs[KEY_NOTIFICATION_TITLE] = it
+ }
+ config.notificationMessage?.let {
+ prefs[KEY_NOTIFICATION_MESSAGE] = it
+ }
+ }
+ }
+
+ suspend fun load(context: Context): TriggerXConfig? {
+ val prefs = context.dataStore.data.firstOrNull() ?: return null
+ val className = prefs[KEY_ACTIVITY_CLASS] ?: return null
+ val title = prefs[KEY_NOTIFICATION_TITLE] ?: "Alarm"
+ val message = prefs[KEY_NOTIFICATION_MESSAGE] ?: "Alarm is ringing"
+
+ return try {
+ val clazz = Class.forName(className) as Class
+ TriggerXConfig().apply {
+ activityClass = clazz
+ notificationTitle = title
+ notificationMessage = message
+ }
+ } catch (e: Exception) {
+ LoggerConfig.logger.e("Failed to load activity class from prefs", e)
+ null
+ }
+ }
+}
diff --git a/triggerx/src/main/java/com/meticha/triggerx/receivers/TriggerXAlarmReceiver.kt b/triggerx/src/main/java/com/meticha/triggerx/receivers/TriggerXAlarmReceiver.kt
new file mode 100644
index 0000000..d1e9b9e
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/receivers/TriggerXAlarmReceiver.kt
@@ -0,0 +1,29 @@
+package com.meticha.triggerx.receivers
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.meticha.triggerx.logger.LoggerConfig
+import com.meticha.triggerx.services.TriggerXForegroundService
+
+class TriggerXAlarmReceiver : BroadcastReceiver() {
+
+ companion object {
+ const val ALARM_ACTION = "com.meticha.triggerx.ALARM_ACTION"
+ }
+
+ override fun onReceive(context: Context, intent: Intent?) {
+ LoggerConfig.logger.d("Alarm received!")
+
+ if (intent?.action == ALARM_ACTION) {
+ val serviceIntent = Intent(context, TriggerXForegroundService::class.java).also {
+ it.action = ALARM_ACTION
+ }
+ context.startForegroundService(serviceIntent)
+ } else {
+ LoggerConfig.logger.w("Received intent with unknown action: ${intent?.action}")
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/triggerx/src/main/java/com/meticha/triggerx/services/TriggerXForegroundService.kt b/triggerx/src/main/java/com/meticha/triggerx/services/TriggerXForegroundService.kt
new file mode 100644
index 0000000..88f3201
--- /dev/null
+++ b/triggerx/src/main/java/com/meticha/triggerx/services/TriggerXForegroundService.kt
@@ -0,0 +1,120 @@
+package com.meticha.triggerx.services
+
+import android.app.Activity
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.IBinder
+import android.os.PowerManager
+import androidx.core.app.NotificationCompat
+import com.meticha.triggerx.DefaultTriggerActivity
+import com.meticha.triggerx.dsl.TriggerX
+import com.meticha.triggerx.logger.LoggerConfig
+import com.meticha.triggerx.preference.TriggerXPreferences
+import com.meticha.triggerx.receivers.TriggerXAlarmReceiver.Companion.ALARM_ACTION
+import kotlinx.coroutines.runBlocking
+import kotlin.jvm.java
+
+class TriggerXForegroundService : Service() {
+ companion object {
+ private const val NOTIFICATION_ID = 1001
+ private const val DEFAULT_CHANNEL_ID = "triggerx_channel"
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ }
+
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+
+ val notification = buildNotification()
+ startForeground(NOTIFICATION_ID, notification)
+
+ // Acquire a wake lock to ensure the device doesn't sleep while processing the alarm
+ val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
+ val wakeLock = powerManager.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ "triggerx::AlarmWakeLock"
+ )
+ // Acquire with a timeout, e.g., 1 minute.
+ // The wakelock will be released in the finally block regardless of timeout.
+ wakeLock.acquire(1 * 60 * 1000L /*1 minute*/)
+ LoggerConfig.logger.d("WakeLock acquired")
+
+ try {
+ if (intent?.action == ALARM_ACTION) {
+ val activityClass = runBlocking { resolveActivityClass(applicationContext) }
+ val activityIntent = Intent(this, activityClass).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ }
+ startActivity(activityIntent)
+ LoggerConfig.logger.d("TriggerActivity started")
+ stopSelf()
+ LoggerConfig.logger.d("stopSelf() called")
+ } else {
+ LoggerConfig.logger.w("Received intent with unknown or missing action: ${intent?.action}")
+ // If the service is started with an unknown action,
+ // it should probably stop itself too.
+ stopSelf()
+ LoggerConfig.logger.d("stopSelf() called due to unknown action")
+ }
+ } catch (e: Exception) {
+ LoggerConfig.logger.e("Error processing alarm", e)
+ // Even if there's an error, try to stop the service.
+ // The finally block will still run.
+ stopSelf()
+ LoggerConfig.logger.d("stopSelf() called after exception")
+ } finally {
+ if (wakeLock.isHeld) {
+ wakeLock.release()
+ LoggerConfig.logger.d("WakeLock released")
+ } else {
+ LoggerConfig.logger.d("WakeLock was not held at finally block.")
+ }
+ }
+ /**
+ * START_NOT_STICKY: If the service is killed by the system,
+ * it will not be automatically restarted.
+ */
+ return START_NOT_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ LoggerConfig.logger.d("Service destroyed")
+ }
+
+ private fun createNotificationChannel() {
+ val channelName = TriggerX.config?.notificationChannelName ?: "TriggerX Alarms"
+ val channel = NotificationChannel(
+ DEFAULT_CHANNEL_ID,
+ channelName,
+ NotificationManager.IMPORTANCE_DEFAULT
+ ).apply {
+ description = "Notifications for TriggerX alarms"
+ }
+ val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ nm.createNotificationChannel(channel)
+ }
+
+ private fun buildNotification(): Notification {
+ return NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID)
+ .setContentTitle(TriggerX.getNotificationTitle())
+ .setContentText(TriggerX.getNotificationMessage())
+ .setOngoing(true)
+ .build()
+ }
+
+ suspend fun resolveActivityClass(context: Context): Class {
+ return TriggerX.config?.activityClass
+ ?: TriggerXPreferences.load(context)?.activityClass
+ ?: DefaultTriggerActivity::class.java
+ }
+}
diff --git a/triggerx/src/test/java/com/meticha/triggerx/ExampleUnitTest.kt b/triggerx/src/test/java/com/meticha/triggerx/ExampleUnitTest.kt
new file mode 100644
index 0000000..e6b9d36
--- /dev/null
+++ b/triggerx/src/test/java/com/meticha/triggerx/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.meticha.triggerx
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file