From f932031cd72d6fed5cf04c784075aec101273e3a Mon Sep 17 00:00:00 2001 From: omer358 Date: Thu, 15 Aug 2024 03:51:32 +0300 Subject: [PATCH 1/6] clean the MainActivity, separate concerns --- .../com/example/rememberme/MainActivity.kt | 111 ++++++++++-------- 1 file changed, 64 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/example/rememberme/MainActivity.kt b/app/src/main/java/com/example/rememberme/MainActivity.kt index 2d7efe5..fe89325 100644 --- a/app/src/main/java/com/example/rememberme/MainActivity.kt +++ b/app/src/main/java/com/example/rememberme/MainActivity.kt @@ -10,6 +10,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.lifecycleScope @@ -23,6 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import java.util.concurrent.TimeUnit + @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -31,55 +33,65 @@ class MainActivity : ComponentActivity() { private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> + ) { isGranted -> + handlePermissionResult(isGranted) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setupUI() + observeUiState() + } + + private fun setupUI() { + enableEdgeToEdge() + installSplashScreen() + setContent { RememberMeApp() } + } + + private fun observeUiState() { lifecycleScope.launch { settingsViewModel.uiState.collect { uiState -> - if (isGranted && uiState.remindersRepetition != lastRepetition) { - // Permission is granted. Schedule the notification work with the correct repetition. - Log.i(TAG, "Permission granted") - scheduleNotificationWork(uiState.remindersRepetition) - lastRepetition = uiState.remindersRepetition - } else { - Log.i(TAG, "Permission denied") - // Permission is denied. Handle the case. + if (shouldScheduleWork(uiState.remindersRepetition)) { + handlePermissionsAndScheduleWork(uiState.remindersRepetition) } } } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - installSplashScreen() + private fun shouldScheduleWork(currentRepetition: RemindersRepetition): Boolean { + return currentRepetition != lastRepetition + } - setContent { - RememberMeApp() + private fun handlePermissionsAndScheduleWork(repetition: RemindersRepetition) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (hasNotificationPermission()) { + scheduleNotificationWork(repetition) + } else { + requestNotificationPermission() + } + } else { + scheduleNotificationWork(repetition) } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun hasNotificationPermission(): Boolean { + return ContextCompat.checkSelfPermission( + this, POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun requestNotificationPermission() { + requestPermissionLauncher.launch(POST_NOTIFICATIONS) + } + private fun handlePermissionResult(isGranted: Boolean) { lifecycleScope.launch { settingsViewModel.uiState.collect { uiState -> - Log.i(TAG, "Settings UI state: $uiState") - val remindersRepetition = uiState.remindersRepetition - - if (remindersRepetition != lastRepetition) { - // Check for notification permission only if the SDK version is 33 or higher - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (ContextCompat.checkSelfPermission( - this@MainActivity, - POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - ) { - // Permission is already granted - scheduleNotificationWork(remindersRepetition) - } else { - // Request permission - requestPermissionLauncher.launch(POST_NOTIFICATIONS) - } - } else { - // SDK version is lower than 33, no need to request POST_NOTIFICATIONS permission - scheduleNotificationWork(remindersRepetition) - } - lastRepetition = remindersRepetition + if (isGranted && shouldScheduleWork(uiState.remindersRepetition)) { + scheduleNotificationWork(uiState.remindersRepetition) } } } @@ -87,28 +99,33 @@ class MainActivity : ComponentActivity() { private fun scheduleNotificationWork(repetition: RemindersRepetition) { Log.i(TAG, "Scheduling notification work with repetition: $repetition") - val repeatInterval = when (repetition) { - RemindersRepetition.OnceADay -> 24 - RemindersRepetition.ThreeADay -> 24 / 3 - RemindersRepetition.FiveADay -> 24 / 5 - } + val repeatInterval = getRepeatIntervalInHours(repetition).toLong() Log.i(TAG, "Scheduling notification work with interval: $repeatInterval hours") val notificationWorkRequest = PeriodicWorkRequestBuilder( - repeatInterval.toLong(), - TimeUnit.HOURS) - .addTag("notificationWorkRequest") - .setInitialDelay(repeatInterval.toLong(), TimeUnit.HOURS) + repeatInterval, TimeUnit.HOURS + ).addTag("notificationWorkRequest") + .setInitialDelay(10, TimeUnit.SECONDS) .build() WorkManager.getInstance(this).enqueueUniquePeriodicWork( "notificationWork", - ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + ExistingPeriodicWorkPolicy.UPDATE, notificationWorkRequest ) + lastRepetition = repetition + } + + private fun getRepeatIntervalInHours(repetition: RemindersRepetition): Int { + return when (repetition) { + RemindersRepetition.OnceADay -> 24 + RemindersRepetition.ThreeADay -> 24 / 3 + RemindersRepetition.FiveADay -> 24 / 5 + } } companion object { private const val TAG = "MainActivity" } } + From 711a57f9a43ee871ac8a21dc66c1e6374f097084 Mon Sep 17 00:00:00 2001 From: omer358 Date: Fri, 16 Aug 2024 19:49:46 +0300 Subject: [PATCH 2/6] enhance the NotificationService --- .../rememberme/data/manager/NotificationService.kt | 11 ++++++----- app/src/main/res/values-ar/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 4 ++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/example/rememberme/data/manager/NotificationService.kt b/app/src/main/java/com/example/rememberme/data/manager/NotificationService.kt index bde4290..9a1b1f1 100644 --- a/app/src/main/java/com/example/rememberme/data/manager/NotificationService.kt +++ b/app/src/main/java/com/example/rememberme/data/manager/NotificationService.kt @@ -18,9 +18,10 @@ import javax.inject.Inject class NotificationService @Inject constructor(private val context: Context) { - private val CHANNEL_ID = "people_notification_channel" - private val NOTIFICATION_ID = 0 - + companion object { + private const val CHANNEL_ID = "people_notification_channel" + private const val NOTIFICATION_ID = 0 + } init { createNotificationChannel() @@ -28,8 +29,8 @@ class NotificationService @Inject constructor(private val context: Context) { private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val name = "People Notification" - val descriptionText = "Reminders about the people you have met" + val name = context.getString(R.string.notification_channel_name) + val descriptionText = context.getString(R.string.notification_channel_description) val importance = NotificationManager.IMPORTANCE_HIGH val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { description = descriptionText diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 9fd6667..01da466 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,4 +1,6 @@ تذكرني + People Reminders + Reminders about the people you have met \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ea9fc5..8c5738f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,7 @@ Remember Me + People Reminders + Reminders about the people you have met + Do you Remember %1$s? + You have met him at %1$s \ No newline at end of file From 2a6a9480dc66d959fc0e488b07a3d8b89524b8d1 Mon Sep 17 00:00:00 2001 From: omer358 Date: Fri, 16 Aug 2024 19:49:46 +0300 Subject: [PATCH 3/6] enhance the NotificationService --- .../data/manager/NotificationService.kt | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/example/rememberme/data/manager/NotificationService.kt b/app/src/main/java/com/example/rememberme/data/manager/NotificationService.kt index 9a1b1f1..e69a95b 100644 --- a/app/src/main/java/com/example/rememberme/data/manager/NotificationService.kt +++ b/app/src/main/java/com/example/rememberme/data/manager/NotificationService.kt @@ -7,11 +7,15 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat import com.example.rememberme.R import com.example.rememberme.domain.model.People import javax.inject.Inject @@ -42,6 +46,14 @@ class NotificationService @Inject constructor(private val context: Context) { } fun showNotification(person: People) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + val deepLinkIntent = Intent( Intent.ACTION_VIEW, Uri.parse("app://people/${person.id}") @@ -53,27 +65,39 @@ class NotificationService @Inject constructor(private val context: Context) { context, 0, deepLinkIntent, - PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) - val builder = NotificationCompat.Builder(context, CHANNEL_ID) + val drawable: Drawable? = ContextCompat.getDrawable(context, person.avatar) + val largeIcon: Bitmap? = drawable?.let { drawableToBitmap(it) } + + val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_m2) - .setContentTitle("Do you remember ${person.firstName}?") - .setContentText("You met at ${person.place}") + .setContentTitle(context.getString(R.string.notification_title, person.firstName)) + .setContentText(context.getString(R.string.notification_text, person.place)) + .setLargeIcon(largeIcon) .setPriority(NotificationCompat.PRIORITY_MAX) - .setChannelId(CHANNEL_ID) .setContentIntent(pendingIntent) .setAutoCancel(true) + .setCategory(NotificationCompat.CATEGORY_REMINDER) + .build() - with(NotificationManagerCompat.from(context)) { - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) != PackageManager.PERMISSION_GRANTED - ) { - return - } - notify(NOTIFICATION_ID, builder.build()) + NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notification) + } + + private fun drawableToBitmap(drawable: Drawable): Bitmap { + return if (drawable is BitmapDrawable) { + drawable.bitmap + } else { + val bitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + val canvas = android.graphics.Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + bitmap } } } From 3068473ddbdd731f09d0a5ab960d9921d99cb24d Mon Sep 17 00:00:00 2001 From: omer358 Date: Fri, 16 Aug 2024 19:57:30 +0300 Subject: [PATCH 4/6] align the reminder options vertically --- .../example/rememberme/presentation/settings/SettingsScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/rememberme/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/example/rememberme/presentation/settings/SettingsScreen.kt index 1d0cdfd..e071c37 100644 --- a/app/src/main/java/com/example/rememberme/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/example/rememberme/presentation/settings/SettingsScreen.kt @@ -269,7 +269,8 @@ fun ReminderOptionRow( modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp) - .clickable { onOptionSelected(repetition) } + .clickable { onOptionSelected(repetition) }, + verticalAlignment = Alignment.CenterVertically ) { RadioButton( selected = (repetition == selectedOption), From a560ef0aa1245857a17f2a9ec7e8c27427847fe2 Mon Sep 17 00:00:00 2001 From: omer358 Date: Fri, 16 Aug 2024 19:58:23 +0300 Subject: [PATCH 5/6] decrease the options spacer by 4.dp --- .../rememberme/presentation/settings/SettingsScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/example/rememberme/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/example/rememberme/presentation/settings/SettingsScreen.kt index e071c37..6bbd44a 100644 --- a/app/src/main/java/com/example/rememberme/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/example/rememberme/presentation/settings/SettingsScreen.kt @@ -254,7 +254,7 @@ fun ThemeOptionRow( selected = (themeMode == selectedOption), onClick = { onOptionSelected(themeMode) } ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(4.dp)) Text(text = themeMode.toString()) } } @@ -276,7 +276,7 @@ fun ReminderOptionRow( selected = (repetition == selectedOption), onClick = { onOptionSelected(repetition) } ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(4.dp)) Text(text = repetition.toString()) } } From 04ea85065af7406c3eae89510b591d3e82b56ac4 Mon Sep 17 00:00:00 2001 From: omer358 Date: Mon, 19 Aug 2024 03:37:02 +0300 Subject: [PATCH 6/6] enhance the transition between screens --- .../presentation/navgraph/NavGraph.kt | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/example/rememberme/presentation/navgraph/NavGraph.kt b/app/src/main/java/com/example/rememberme/presentation/navgraph/NavGraph.kt index b88df3c..d4ea2fd 100644 --- a/app/src/main/java/com/example/rememberme/presentation/navgraph/NavGraph.kt +++ b/app/src/main/java/com/example/rememberme/presentation/navgraph/NavGraph.kt @@ -1,8 +1,7 @@ package com.example.rememberme.presentation.navgraph import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.core.EaseIn -import androidx.compose.animation.core.EaseOut +import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.runtime.Composable import androidx.navigation.NavHostController @@ -20,6 +19,7 @@ import com.example.rememberme.presentation.peopleList.PeopleScreen import com.example.rememberme.presentation.settings.SettingsScreen private const val TAG = "NavGraph" + @Composable fun NavGraph( startDestination: String, @@ -29,15 +29,28 @@ fun NavGraph( navController = navController, startDestination = startDestination, enterTransition = { - slideIntoContainer( - animationSpec = tween(300, easing = EaseIn), - towards = AnimatedContentTransitionScope.SlideDirection.Start + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(700, easing = FastOutSlowInEasing) + ) }, exitTransition = { slideOutOfContainer( - animationSpec = tween(300, easing = EaseOut), - towards = AnimatedContentTransitionScope.SlideDirection.End + AnimatedContentTransitionScope.SlideDirection.Start, + animationSpec = tween(700) + ) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(700) + ) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.End, + animationSpec = tween(700) ) } ) { @@ -78,7 +91,9 @@ fun NavGraph( } composable( route = "${Routes.AddPersonScreen.route}/{personId}", - arguments = listOf(navArgument("personId") { type = NavType.StringType; nullable = true }) + arguments = listOf(navArgument("personId") { + type = NavType.StringType; nullable = true + }) ) { val personId = it.arguments?.getString("personId")?.toLongOrNull() @@ -101,7 +116,7 @@ fun NavGraph( navigateUp = { navController.navigateUp() }, - navigateToEditScreen = {id -> + navigateToEditScreen = { id -> navController.navigate("${Routes.AddPersonScreen.route}/$id") } ) @@ -109,11 +124,11 @@ fun NavGraph( } composable(route = Routes.SettingsScreen.route) { SettingsScreen( - popUp ={ + popUp = { navController.navigateUp() } ) } } } -} +} \ No newline at end of file