Skip to content

Commit

Permalink
πŸ”€ Merge pull request #8 from lorenzovngl/5-implement-notification
Browse files Browse the repository at this point in the history
✨ Implement notifications
  • Loading branch information
lorenzovngl authored May 28, 2023
2 parents 8c3fd57 + 0e41511 commit 782f1e5
Show file tree
Hide file tree
Showing 16 changed files with 400 additions and 48 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/deploymentTargetDropDown.xml
.DS_Store
/build
/captures
Expand Down
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ dependencies {
// To use Kotlin annotation processing tool (kapt)
kapt "androidx.room:room-compiler:$room_version"

implementation "androidx.work:work-runtime-ktx:2.8.1"

if (firebaseEnabled) {
// Firebase
// https://firebase.google.com/docs/android/setup#available-libraries
Expand Down
7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />

<application
Expand Down Expand Up @@ -34,6 +35,10 @@
<activity
android:name=".view.SettingsActivity">
</activity>
<service
android:name=".model.worker.CheckExpirationsWorker"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.lorenzovainigli.foodexpirationdates.model

import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.lorenzovainigli.foodexpirationdates.BuildConfig

class NotificationManager {

companion object {

private const val channelId = "channel_reminders"
private const val channelName = "Reminders"
private var permissionGranted = false

fun setupNotificationChannel(activity: ComponentActivity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
channelName,
NotificationManager.IMPORTANCE_HIGH
)
val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
notificationManager.createNotificationChannel(channel)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionGranted = activity.let {
ContextCompat.checkSelfPermission(
it.applicationContext,
Manifest.permission.POST_NOTIFICATIONS
)
} == PackageManager.PERMISSION_GRANTED
}
val requestPermissionLauncher =
activity.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (BuildConfig.DEBUG) {
if (isGranted) {
Toast.makeText(activity, "Permission granted", Toast.LENGTH_SHORT)
.show()
} else {
Toast.makeText(activity, "Permission not granted", Toast.LENGTH_SHORT)
.show()
}
}
permissionGranted = isGranted
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale

class DateFormatProvider {
class PreferencesProvider {

companion object {

private const val sharedPrefsName = "shared_pref"
private const val keyDateFormat = "date_format"
private const val keyNotificationTimeHour = "notification_time_hour"
private const val keyNotificationTimeMinute = "notification_time_minute"
private val availLocaleDateFormats = arrayOf(DateFormat.SHORT, DateFormat.MEDIUM)
private val availOtherDateFormats =
arrayOf(
Expand Down Expand Up @@ -46,5 +48,20 @@ class DateFormatProvider {
.edit().putString(keyDateFormat, dateFormat).apply()
}

fun getUserNotificationTimeHour(context: Context): Int {
return context.getSharedPreferences(sharedPrefsName, ComponentActivity.MODE_PRIVATE)
.getInt(keyNotificationTimeHour, 11)
}

fun getUserNotificationTimeMinute(context: Context): Int {
return context.getSharedPreferences(sharedPrefsName, ComponentActivity.MODE_PRIVATE)
.getInt(keyNotificationTimeMinute, 0)
}

fun setUserNotificationTime(context: Context, hour: Int, minute: Int) {
return context.getSharedPreferences(sharedPrefsName, ComponentActivity.MODE_PRIVATE)
.edit().putInt(keyNotificationTimeHour, hour)
.putInt(keyNotificationTimeMinute, minute).apply()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.lorenzovainigli.foodexpirationdates.model.worker

import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.view.MainActivity

class CheckExpirationsWorker(
appContext: Context, params: WorkerParameters
) : CoroutineWorker(appContext, params) {

override suspend fun doWork(): Result {
val message = inputData.getString("message")
if (message != null) {
showNotification(
title = applicationContext.getString(R.string.your_food_is_expiring),
message = message
)
}
return Result.success()
}

private fun showNotification(title: String, message: String) {
if (ContextCompat.checkSelfPermission(
applicationContext,
android.Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED) {
val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
val intent = Intent(applicationContext, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val notification = NotificationCompat.Builder(applicationContext, "channel_reminders")
.setContentText(message)
.setContentTitle(title)
.setSmallIcon(R.drawable.fed_icon)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
notificationManager.notify(1, notification)
}
}

companion object {
const val workerID = "DailyExpirationCheck"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,32 @@ package com.lorenzovainigli.foodexpirationdates.view
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
Expand All @@ -22,19 +39,30 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.Wallpapers
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import com.lorenzovainigli.foodexpirationdates.BuildConfig
import com.lorenzovainigli.foodexpirationdates.R
import com.lorenzovainigli.foodexpirationdates.di.AppModule
import com.lorenzovainigli.foodexpirationdates.di.DaggerAppComponent
import com.lorenzovainigli.foodexpirationdates.view.composable.DropdownMenu
import com.lorenzovainigli.foodexpirationdates.model.NotificationManager
import com.lorenzovainigli.foodexpirationdates.model.PreferencesProvider
import com.lorenzovainigli.foodexpirationdates.model.entity.ExpirationDate
import com.lorenzovainigli.foodexpirationdates.ui.theme.*
import com.lorenzovainigli.foodexpirationdates.model.worker.CheckExpirationsWorker
import com.lorenzovainigli.foodexpirationdates.ui.theme.FoodExpirationDatesTheme
import com.lorenzovainigli.foodexpirationdates.view.composable.DropdownMenu
import com.lorenzovainigli.foodexpirationdates.view.composable.FoodCard
import com.lorenzovainigli.foodexpirationdates.view.composable.MyTopAppBar
import com.lorenzovainigli.foodexpirationdates.viewmodel.ExpirationDateViewModel
import dagger.hilt.android.AndroidEntryPoint
import java.util.*
import kotlin.collections.ArrayList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import java.util.Calendar
import java.util.concurrent.TimeUnit
import kotlin.math.min

@AndroidEntryPoint
Expand All @@ -45,6 +73,7 @@ class MainActivity : ComponentActivity() {
DaggerAppComponent.builder()
.appModule(AppModule())
.build()
NotificationManager.setupNotificationChannel(this)
}

override fun onResume() {
Expand All @@ -63,6 +92,9 @@ class MainActivity : ComponentActivity() {
deleteExpirationDate: ((ExpirationDate) -> Unit)? = viewModel!!::deleteExpirationDate,
) {
val context = LocalContext.current
if (viewModel != null) {
scheduleDailyCheckOfExpirations(viewModel.getDates())
}
FoodExpirationDatesTheme {
Surface(
modifier = Modifier
Expand Down Expand Up @@ -178,6 +210,98 @@ class MainActivity : ComponentActivity() {
}
}

private fun scheduleDailyCheckOfExpirations(dates: Flow<List<ExpirationDate>>) {
lifecycleScope.launch {
val sb = StringBuilder()
dates.collect { list ->
val today = Calendar.getInstance()
val twoDaysAgo = Calendar.getInstance()
twoDaysAgo.add(Calendar.DAY_OF_MONTH, -2)
val yesterday = Calendar.getInstance()
yesterday.add(Calendar.DAY_OF_MONTH, -1)
val tomorrow = Calendar.getInstance()
tomorrow.add(Calendar.DAY_OF_MONTH, 1)
val msInADay = (1000 * 60 * 60 * 24)
val filteredList = list.filter {
it.expirationDate < tomorrow.time.time
}
if (filteredList.isEmpty()){
return@collect
}
filteredList.map {
sb.append(it.foodName).append(" (")
if (it.expirationDate < twoDaysAgo.time.time) {
val days = (today.time.time - it.expirationDate) / msInADay
sb.append(getString(R.string.n_days_ago, days))
} else if (it.expirationDate < yesterday.time.time)
sb.append(getString(R.string.yesterday).lowercase())
else if (it.expirationDate < today.time.time){
sb.append(getString(R.string.today).lowercase())
} else {
sb.append(getString(R.string.tomorrow).lowercase())
}
sb.append("), ")
}
val currentTime = Calendar.getInstance()
val dueTime = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, PreferencesProvider.getUserNotificationTimeHour(
applicationContext))
set(Calendar.MINUTE, PreferencesProvider.getUserNotificationTimeMinute(
applicationContext))
set(Calendar.SECOND, 0)
}
if (currentTime > dueTime)
dueTime.add(Calendar.DAY_OF_MONTH, 1)
var message = ""
if (sb.toString().length > 2)
message = sb.toString().substring(0, sb.toString().length - 2) + "."
val inputData = workDataOf("message" to message)
val initialDelay = dueTime.timeInMillis - currentTime.timeInMillis
val formattedTime = formatTimeDifference(initialDelay)
if (BuildConfig.DEBUG) {
Toast.makeText(
applicationContext,
"Notification in $formattedTime",
Toast.LENGTH_SHORT
).show()
}
val workRequest = PeriodicWorkRequestBuilder<CheckExpirationsWorker>(
1, TimeUnit.DAYS
)
.setInputData(inputData)
.setInitialDelay(initialDelay, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(applicationContext)
.enqueueUniquePeriodicWork(
CheckExpirationsWorker.workerID,
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
workRequest
)
}
}
}

private fun formatTimeDifference(timeDifference: Long): String {
val days = TimeUnit.MILLISECONDS.toDays(timeDifference)
val hours = TimeUnit.MILLISECONDS.toHours(timeDifference) % 24
val minutes = TimeUnit.MILLISECONDS.toMinutes(timeDifference) % 60
val seconds = TimeUnit.MILLISECONDS.toSeconds(timeDifference) % 60
val formattedTime = StringBuilder()
if (days > 0) {
formattedTime.append("$days day${if (days > 1) "s" else ""} ")
}
if (hours > 0) {
formattedTime.append("$hours hour${if (hours > 1) "s" else ""} ")
}
if (minutes > 0) {
formattedTime.append("$minutes minute${if (minutes > 1) "s" else ""} ")
}
if (seconds > 0) {
formattedTime.append("$seconds second${if (seconds > 1) "s" else ""} ")
}
return formattedTime.toString().trim()
}

private fun getItemsForPreview(): List<ExpirationDate> {
val items = ArrayList<ExpirationDate>()
val foods = resources.getStringArray(R.array.example_foods)
Expand Down
Loading

0 comments on commit 782f1e5

Please sign in to comment.