Skip to content

Commit 54b4350

Browse files
committed
feat: notifications
1 parent bfc4686 commit 54b4350

File tree

28 files changed

+917
-59
lines changed

28 files changed

+917
-59
lines changed

.github/workflows/build.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,11 @@ jobs:
2626
run: chmod +x ./gradlew
2727

2828
- name: Build with Gradle
29-
run: ./gradlew detekt assembleDebug
29+
run: ./gradlew detekt assembleRelease
30+
31+
- name: Upload the universal artifact
32+
uses: actions/upload-artifact@v4
33+
with:
34+
path: ./composeApp/build/outputs/apk/release/composeApp-release-unsigned.apk
35+
name: progres
36+

composeApp/src/androidMain/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<!-- Remove unnecessary permissions from Firebase dependency -->
1212
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID" tools:node="remove" />
1313
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" tools:node="remove" />
14+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
1415
<uses-permission android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE" tools:node="remove" />
1516
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove" />
1617

composeApp/src/androidMain/kotlin/mehiz/abdallah/progres/App.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class App : Application(), KoinComponent {
1515

1616
override fun onCreate() {
1717
super.onCreate()
18+
1819
startKoin {
1920
androidContext(applicationContext)
2021
workManagerFactory()

composeApp/src/androidMain/kotlin/mehiz/abdallah/progres/MainActivity.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import preferences.preference.collectAsState
2626
import presentation.theme.DarkMode
2727
import utils.AuthRefreshWorker
2828
import utils.PlatformUtils
29+
import utils.UpdatesWorkers
2930
import java.time.Duration
3031
import java.util.Locale
3132
import java.util.concurrent.TimeUnit
@@ -51,13 +52,15 @@ class MainActivity : ComponentActivity() {
5152
}
5253
.build()
5354

55+
UpdatesWorkers.scheduleCCGradesUpdateWork(this)
56+
UpdatesWorkers.scheduleExamGradesUpdateWork(this)
57+
UpdatesWorkers.scheduleTranscriptsUpdateWork(this)
58+
UpdatesWorkers.scheduleAppUpdateCheckWork(this)
59+
UpdatesWorkers.runAppUpdateCheckWorkImmediately(this)
60+
5461
WorkManager
5562
.getInstance(applicationContext)
56-
.enqueueUniquePeriodicWork(
57-
"auth_refresh",
58-
ExistingPeriodicWorkPolicy.UPDATE,
59-
authRefreshWorker,
60-
)
63+
.enqueueUniquePeriodicWork("auth_refresh", ExistingPeriodicWorkPolicy.UPDATE, authRefreshWorker)
6164

6265
setContent {
6366
val preferences = koinInject<BasePreferences>()

composeApp/src/androidMain/kotlin/mehiz/abdallah/progres/di/WorkersModule.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ package mehiz.abdallah.progres.di
33
import org.koin.androidx.workmanager.dsl.workerOf
44
import org.koin.dsl.module
55
import utils.AuthRefreshWorker
6+
import utils.workers.AppUpdateCheckWorker
7+
import utils.workers.CCGradesUpdateWorker
8+
import utils.workers.ExamGradesUpdateWorker
9+
import utils.workers.TranscriptsUpdateWorker
610

711
val WorkersModule = module {
812
workerOf(::AuthRefreshWorker)
13+
workerOf(::CCGradesUpdateWorker)
14+
workerOf(::ExamGradesUpdateWorker)
15+
workerOf(::TranscriptsUpdateWorker)
16+
workerOf(::AppUpdateCheckWorker)
917
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package presentation
2+
3+
import android.Manifest
4+
import android.content.Intent
5+
import android.net.Uri
6+
import android.os.Build
7+
import android.provider.Settings
8+
import androidx.activity.compose.rememberLauncherForActivityResult
9+
import androidx.activity.result.contract.ActivityResultContracts
10+
import androidx.compose.animation.AnimatedVisibility
11+
import androidx.compose.animation.fadeIn
12+
import androidx.compose.animation.fadeOut
13+
import androidx.compose.foundation.background
14+
import androidx.compose.foundation.clickable
15+
import androidx.compose.foundation.layout.Row
16+
import androidx.compose.foundation.layout.fillMaxWidth
17+
import androidx.compose.foundation.layout.padding
18+
import androidx.compose.foundation.shape.RoundedCornerShape
19+
import androidx.compose.material.icons.Icons
20+
import androidx.compose.material.icons.rounded.NotificationsOff
21+
import androidx.compose.material3.Icon
22+
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.Text
24+
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.LaunchedEffect
26+
import androidx.compose.runtime.getValue
27+
import androidx.compose.runtime.mutableStateOf
28+
import androidx.compose.runtime.remember
29+
import androidx.compose.runtime.setValue
30+
import androidx.compose.ui.Modifier
31+
import androidx.compose.ui.draw.clip
32+
import androidx.compose.ui.platform.LocalContext
33+
import androidx.compose.ui.unit.dp
34+
import androidx.core.app.ActivityCompat
35+
import androidx.core.content.ContextCompat
36+
import dev.icerock.moko.resources.compose.stringResource
37+
import kotlinx.coroutines.delay
38+
import mehiz.abdallah.progres.i18n.MR
39+
40+
@Composable
41+
actual fun NotificationPromptTrigger(
42+
modifier: Modifier,
43+
) {
44+
val context = LocalContext.current
45+
46+
var hasNotificationsPermission by remember { mutableStateOf(true) }
47+
48+
LaunchedEffect(Unit) {
49+
while (true) {
50+
hasNotificationsPermission = if (Build.VERSION.SDK_INT >= 33) {
51+
ContextCompat.checkSelfPermission(
52+
context,
53+
Manifest.permission.POST_NOTIFICATIONS,
54+
) == android.content.pm.PackageManager.PERMISSION_GRANTED
55+
} else {
56+
true
57+
}
58+
delay(1000)
59+
}
60+
}
61+
62+
val wasPreviouslyDenied = remember {
63+
mutableStateOf(
64+
if (Build.VERSION.SDK_INT >= 33) {
65+
ActivityCompat.shouldShowRequestPermissionRationale(
66+
context.findActivity(),
67+
Manifest.permission.POST_NOTIFICATIONS,
68+
)
69+
} else {
70+
false
71+
},
72+
)
73+
}
74+
75+
val permissionLauncher = rememberLauncherForActivityResult(
76+
contract = ActivityResultContracts.RequestPermission(),
77+
onResult = { isGranted ->
78+
hasNotificationsPermission = isGranted
79+
if (!isGranted) {
80+
wasPreviouslyDenied.value = true
81+
}
82+
},
83+
)
84+
85+
AnimatedVisibility(
86+
visible = !hasNotificationsPermission,
87+
modifier = modifier,
88+
fadeIn(),
89+
fadeOut()
90+
) {
91+
Row(
92+
modifier = Modifier
93+
.clip(RoundedCornerShape(16.dp))
94+
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
95+
.fillMaxWidth()
96+
.clickable {
97+
if (Build.VERSION.SDK_INT >= 33 && !wasPreviouslyDenied.value) {
98+
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
99+
} else {
100+
val intent = Intent().apply {
101+
if (Build.VERSION.SDK_INT >= 26) {
102+
action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
103+
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
104+
} else {
105+
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
106+
data = Uri.fromParts("package", context.packageName, null)
107+
}
108+
}
109+
context.startActivity(intent)
110+
}
111+
}
112+
.padding(16.dp),
113+
) {
114+
Icon(
115+
Icons.Rounded.NotificationsOff,
116+
contentDescription = stringResource(MR.strings.enable_notifications),
117+
tint = MaterialTheme.colorScheme.primary,
118+
)
119+
Text(
120+
stringResource(MR.strings.enable_notifications),
121+
modifier = Modifier.padding(horizontal = 8.dp),
122+
)
123+
}
124+
}
125+
}
126+
127+
// Extension function to find the activity from a context
128+
private fun android.content.Context.findActivity(): android.app.Activity {
129+
var context = this
130+
while (context is android.content.ContextWrapper) {
131+
if (context is android.app.Activity) return context
132+
context = context.baseContext
133+
}
134+
throw IllegalStateException("Couldn't find activity from context")
135+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package utils
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import androidx.work.Constraints
6+
import androidx.work.ExistingPeriodicWorkPolicy
7+
import androidx.work.ExistingWorkPolicy
8+
import androidx.work.NetworkType
9+
import androidx.work.OneTimeWorkRequestBuilder
10+
import androidx.work.PeriodicWorkRequestBuilder
11+
import androidx.work.WorkManager
12+
import mehiz.abdallah.progres.core.TAG
13+
import utils.workers.AppUpdateCheckWorker
14+
import utils.workers.CCGradesUpdateWorker
15+
import utils.workers.ExamGradesUpdateWorker
16+
import utils.workers.TranscriptsUpdateWorker
17+
import java.time.Duration
18+
import java.util.concurrent.TimeUnit
19+
20+
object UpdatesWorkers {
21+
private const val EXAM_GRADES_UPDATE_WORK = "exam_grades_update_work"
22+
private const val EXAM_GRADES_IMMEDIATE_WORK = "exam_grades_immediate_work"
23+
private const val TRANSCRIPTS_UPDATE_WORK = "transcripts_update_work"
24+
private const val TRANSCRIPTS_IMMEDIATE_WORK = "transcripts_immediate_work"
25+
private const val CC_GRADES_UPDATE_WORK = "cc_grades_update_work"
26+
private const val CC_GRADES_IMMEDIATE_WORK = "cc_grades_immediate_work"
27+
28+
29+
fun scheduleExamGradesUpdateWork(context: Context) {
30+
val constraints = Constraints.Builder()
31+
.setRequiredNetworkType(NetworkType.CONNECTED)
32+
.build()
33+
34+
val examGradesUpdateRequest =
35+
PeriodicWorkRequestBuilder<ExamGradesUpdateWorker>(Duration.ofHours(1))
36+
.setConstraints(constraints)
37+
.build()
38+
39+
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
40+
EXAM_GRADES_UPDATE_WORK,
41+
ExistingPeriodicWorkPolicy.KEEP,
42+
examGradesUpdateRequest,
43+
)
44+
45+
runExamGradesUpdateWorkImmediately(context)
46+
}
47+
48+
fun runExamGradesUpdateWorkImmediately(context: Context) {
49+
val constraints = Constraints.Builder()
50+
.setRequiredNetworkType(NetworkType.CONNECTED)
51+
.build()
52+
53+
val examGradesUpdateRequest = OneTimeWorkRequestBuilder<ExamGradesUpdateWorker>()
54+
.setConstraints(constraints)
55+
.build()
56+
57+
WorkManager.getInstance(context).enqueueUniqueWork(
58+
EXAM_GRADES_IMMEDIATE_WORK,
59+
ExistingWorkPolicy.REPLACE,
60+
examGradesUpdateRequest,
61+
)
62+
63+
Log.d(TAG, "Scheduled immediate exam grades update for testing")
64+
}
65+
66+
fun scheduleTranscriptsUpdateWork(context: Context) {
67+
val constraints = Constraints.Builder()
68+
.setRequiredNetworkType(NetworkType.CONNECTED)
69+
.build()
70+
71+
val transcriptsUpdateRequest =
72+
PeriodicWorkRequestBuilder<TranscriptsUpdateWorker>(Duration.ofHours(1))
73+
.setConstraints(constraints)
74+
.build()
75+
76+
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
77+
TRANSCRIPTS_UPDATE_WORK,
78+
ExistingPeriodicWorkPolicy.KEEP,
79+
transcriptsUpdateRequest
80+
)
81+
runTranscriptsUpdateWorkImmediately(context)
82+
}
83+
84+
fun runTranscriptsUpdateWorkImmediately(context: Context) {
85+
val constraints = Constraints.Builder()
86+
.setRequiredNetworkType(NetworkType.CONNECTED)
87+
.build()
88+
89+
val transcriptsUpdateRequest = OneTimeWorkRequestBuilder<TranscriptsUpdateWorker>()
90+
.setConstraints(constraints)
91+
.build()
92+
93+
WorkManager.getInstance(context).enqueueUniqueWork(
94+
TRANSCRIPTS_IMMEDIATE_WORK,
95+
ExistingWorkPolicy.REPLACE,
96+
transcriptsUpdateRequest
97+
)
98+
99+
Log.d(TAG, "Scheduled immediate transcripts update for testing")
100+
}
101+
102+
fun scheduleCCGradesUpdateWork(context: Context) {
103+
val constraints = Constraints.Builder()
104+
.setRequiredNetworkType(NetworkType.CONNECTED)
105+
.build()
106+
107+
val ccGradesUpdateRequest =
108+
PeriodicWorkRequestBuilder<CCGradesUpdateWorker>(Duration.ofHours(1))
109+
.setConstraints(constraints)
110+
.build()
111+
112+
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
113+
CC_GRADES_UPDATE_WORK,
114+
ExistingPeriodicWorkPolicy.KEEP,
115+
ccGradesUpdateRequest
116+
)
117+
118+
runCCGradesUpdateWorkImmediately(context)
119+
}
120+
121+
fun runCCGradesUpdateWorkImmediately(context: Context) {
122+
val constraints = Constraints.Builder()
123+
.setRequiredNetworkType(NetworkType.CONNECTED)
124+
.build()
125+
126+
val ccGradesUpdateRequest = OneTimeWorkRequestBuilder<CCGradesUpdateWorker>()
127+
.setConstraints(constraints)
128+
.build()
129+
130+
WorkManager.getInstance(context).enqueueUniqueWork(
131+
CC_GRADES_IMMEDIATE_WORK,
132+
ExistingWorkPolicy.REPLACE,
133+
ccGradesUpdateRequest
134+
)
135+
136+
Log.d(TAG, "Scheduled immediate CC grades update for testing")
137+
}
138+
139+
fun scheduleAppUpdateCheckWork(context: Context) {
140+
val appUpdateCheckRequest = PeriodicWorkRequestBuilder<AppUpdateCheckWorker>(
141+
repeatInterval = 24,
142+
repeatIntervalTimeUnit = TimeUnit.HOURS
143+
).build()
144+
145+
WorkManager.getInstance(context)
146+
.enqueueUniquePeriodicWork(
147+
"app_update_check",
148+
ExistingPeriodicWorkPolicy.UPDATE,
149+
appUpdateCheckRequest
150+
)
151+
}
152+
153+
fun runAppUpdateCheckWorkImmediately(context: Context) {
154+
val appUpdateCheckRequest = OneTimeWorkRequestBuilder<AppUpdateCheckWorker>().build()
155+
156+
WorkManager.getInstance(context)
157+
.enqueueUniqueWork(
158+
"app_update_check_immediate",
159+
ExistingWorkPolicy.REPLACE,
160+
appUpdateCheckRequest
161+
)
162+
}
163+
}

0 commit comments

Comments
 (0)