Skip to content

Commit

Permalink
[JJ-16] Record Jog (#52)
Browse files Browse the repository at this point in the history
* Created notification update function to update notification with a timer

* Created Timer in foreground service notification

* Fixed issue of notification noise playing everytime notification is updated

* Added work manager with coroutine support, fixed DAO setup, created skeleton workmanager for jog entries

* Fixed dao function refactoring to make sure it's reflected across app

* Extremely huge change

* Fixed minor issues

* Stores jog entries, jog summary temp, and jog summary. But coroutine constantly runs even when application is dead. Ensure coroutine cancellation

* Fixed issue of infinite work manager requests, missing requests, etc

* Testing

* Changed JogSummaryWorker structure to contain async blocks

* removed testing

* Fixed duration not updating for jogSummary'

* Fixed notifications
  • Loading branch information
RamziJabali authored May 31, 2024
1 parent 15bea0f commit dbe036f
Show file tree
Hide file tree
Showing 27 changed files with 590 additions and 80 deletions.
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.play.services.maps)
coreLibraryDesugaring(libs.com.android.desugaring)
implementation(libs.org.jetbrains.ktx)
implementation(libs.org.jetbrains.core.ktx)
Expand All @@ -76,8 +77,8 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.material3.icons)
implementation(libs.androidx.compose.foundation)
implementation(libs.io.github.boguszpawlowski.composecalendar.kotlinx.datetime)
implementation(libs.io.github.boguszpawlowski.composecalendar)
implementation(libs.work.manager)
implementation(libs.koin.di)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:name=".notification.NotificationUtil"
android:name=".util.JustJogApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/ramzi/eljabali/justjog/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,24 @@ import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.core.content.ContextCompat
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import ramzi.eljabali.justjog.loactionservice.ForegroundService
import ramzi.eljabali.justjog.notification.permissions
import ramzi.eljabali.justjog.util.permissions
import ramzi.eljabali.justjog.ui.design.JustJogTheme
import ramzi.eljabali.justjog.ui.views.JoggingFAB
import ramzi.eljabali.justjog.ui.util.JustJogNavigation
import ramzi.eljabali.justjog.ui.views.BottomNavigationView
import ramzi.eljabali.justjog.usecase.JogUseCase

class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissionStatus()

val jogUseCase by inject<JogUseCase>()
setContent {
val navController = rememberNavController()
JustJogTheme(true) {
Expand Down
32 changes: 32 additions & 0 deletions app/src/main/java/ramzi/eljabali/justjog/koin/Module.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ramzi.eljabali.justjog.koin

import androidx.room.Room
import org.koin.android.ext.koin.androidApplication
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import ramzi.eljabali.justjog.repository.room.database.JustJogDataBase
import ramzi.eljabali.justjog.repository.room.jogentries.JogEntryDAO
import ramzi.eljabali.justjog.repository.room.jogsummary.JogSummaryDAO
import ramzi.eljabali.justjog.repository.room.jogsummarytemp.JogSummaryTempDAO
import ramzi.eljabali.justjog.usecase.JogUseCase
import ramzi.eljabali.justjog.viewmodel.JogViewModel

val statisticsModule = module {
viewModel { JogViewModel() }
}

val jogDataBaseModule = module {
single<JustJogDataBase> {
Room.databaseBuilder(
androidApplication().applicationContext,
JustJogDataBase::class.java, "just-jog-database"
).build()
}
single<JogEntryDAO> { get<JustJogDataBase>().jogEntryDao() }
single<JogSummaryDAO> { get<JustJogDataBase>().jogSummaryDao() }
single<JogSummaryTempDAO> { get<JustJogDataBase>().jogSummaryTempDao() }
}

val jogUseCaseModule = module {
single { JogUseCase(get(), get(), get()) }
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,64 @@
package ramzi.eljabali.justjog.loactionservice

import android.Manifest
import android.app.ForegroundServiceStartNotAllowedException
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import javatimefun.zoneddatetime.ZonedDateTimes
import javatimefun.zoneddatetime.extensions.print
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import ramzi.eljabali.justjog.MainActivity
import ramzi.eljabali.justjog.R
import ramzi.eljabali.justjog.notification.permissions
import ramzi.eljabali.justjog.util.permissions
import ramzi.eljabali.justjog.usecase.JogUseCase
import ramzi.eljabali.justjog.util.DateFormat
import ramzi.eljabali.justjog.util.TAG
import ramzi.eljabali.justjog.util.formatDuration
import ramzi.eljabali.justjog.workmanager.JogSummaryWorkManager
import java.time.Duration
import java.time.ZonedDateTime

class ForegroundService : Service() {
companion object {
private const val NOTIFICATION_ID = 1
private const val LOCATION_REQUEST_INTERVAL_MS = 2000L
private const val LOCATION_REQUEST_INTERVAL_MS = 1000L
private const val CHANNEL_ID_1 = "JUST_JOG_1"
private const val JOG_TRACKER_WORKER_ID = "JOG_TRACKER_WORKER_ID"
}

private val jogUseCase by inject<JogUseCase>()
private val jogStartZonedDateTime by lazy { ZonedDateTimes.now }

private val locationManager by lazy {
ContextCompat.getSystemService(application, LocationManager::class.java) as LocationManager
}

private var id = 0
private var lastWorkRequestTime = 0L
private val locationListener: LocationListener by lazy {
LocationListener { location ->
Log.d(
"ForegroundService::Class",
"Time:${ZonedDateTime.now()}\nLatitude: ${location.latitude}, Longitude:${location.longitude}"
)
val duration = Duration.between(jogStartZonedDateTime, ZonedDateTime.now())
updateNotification(formatDuration(duration))
recordRunEvent(id, location)
}
}

Expand Down Expand Up @@ -76,6 +99,10 @@ class ForegroundService : Service() {

private fun stop() {
locationManager.removeUpdates(locationListener)
WorkManager.getInstance(applicationContext).cancelAllWorkByTag(JOG_TRACKER_WORKER_ID)
CoroutineScope(Dispatchers.IO).launch {
jogUseCase.deleteAllTempJogSummaries()
}
stopSelf()
}

Expand All @@ -86,29 +113,29 @@ class ForegroundService : Service() {
if (currentPermission == PackageManager.PERMISSION_DENIED) {
// Without these permissions the service cannot run in the foreground
// Consider informing user or updating your app UI if visible.
Log.d("ForegroundService::Class", "Permissions were not given, stopping service!")
stopSelf()
Log.e("ForegroundService::Class", "Permissions were not given, stopping service!")
stop()
return
}
}

try {
ServiceCompat.startForeground(
this,
NOTIFICATION_ID, // Cannot be 0
getNotification(),
getNotification(
ContextCompat.getString(
applicationContext,
R.string.initializing_jog
)
),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
} else {
0
},
)
// getting user location
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
LOCATION_REQUEST_INTERVAL_MS,
0F,
locationListener
)
getJogIDAndStartTrackingJog()
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& e is ForegroundServiceStartNotAllowedException
Expand All @@ -122,16 +149,17 @@ class ForegroundService : Service() {


// ~~~ Notification Builder ~~~
private fun getNotification() =
private fun getNotification(time: String) =
NotificationCompat.Builder(applicationContext, CHANNEL_ID_1)
.setSmallIcon(R.mipmap.just_jog_icon_foreground)
.setContentTitle(ContextCompat.getString(applicationContext, R.string.just_jog))
.setContentText(ContextCompat.getString(applicationContext, R.string.notification_text))
.setContentTitle(ContextCompat.getString(applicationContext, R.string.timer))
.setContentText(time)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOngoing(true)
.setAutoCancel(false)
.setStyle(NotificationCompat.BigTextStyle())
.setOnlyAlertOnce(true)
// Set the intent that fires when the user taps the notification.
.setContentIntent(pendingIntent)
.addAction(
Expand All @@ -141,6 +169,76 @@ class ForegroundService : Service() {
)
.build()

private fun updateNotification(content: String) {
val notification = getNotification(content)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, notification)
}

// jog
private fun getJogIDAndStartTrackingJog() {
Log.i(TAG, "In getJogIDAndStartTrackingJog")
CoroutineScope(Dispatchers.Main).launch {
try {
Log.i(TAG, "Getting new run ID")
val newId = jogUseCase.getNewJogID()
Log.i(TAG, "Got new run id $newId")
// Proceed with starting the tracking jog
setJogListener(newId)
} catch (e: Exception) {
// Handle the error
Log.e(TAG, "Error getting new run ID", e)
}
}
}

private fun setJogListener(newId: Int) {
// getting user location
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
stop()
return
}
id = newId
Log.i(TAG, "Starting location updates")
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
LOCATION_REQUEST_INTERVAL_MS,
0F,
locationListener
)
}

private fun recordRunEvent(id: Int, location: Location) {
Log.i(TAG, "Success getting location")

val currentTime = System.currentTimeMillis()
if (currentTime - lastWorkRequestTime < LOCATION_REQUEST_INTERVAL_MS) {
// Skip this event if it is too soon since the last event
return
}
lastWorkRequestTime = currentTime
val data = Data.Builder().apply {
putDouble("KEY_LONGITUDE", location.longitude)
putDouble("KEY_LATITUDE", location.latitude)
putString("DATE_TIME", ZonedDateTime.now().print(DateFormat.YYYY_MM_DD_T_TIME.format))
putInt("ID", id)
}.build()

val recordSummaryWorker =
OneTimeWorkRequest.Builder(JogSummaryWorkManager::class.java).apply {
setInputData(data)
addTag(JOG_TRACKER_WORKER_ID)
}.build()

WorkManager.getInstance(applicationContext).enqueue(recordSummaryWorker)
}

// Actions
enum class Actions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import androidx.room.PrimaryKey
@Entity("jog_entries")
data class JogEntry(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id")
val id: Int,
val id: Int = 0,
@ColumnInfo("jog_summary_id")
val jogSummaryId: Int,
@ColumnInfo("date_time")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,22 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow

@Dao
interface JogEntryDAO {

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addUpdateWorkout(jogEntries: JogEntry)
fun addEntry(jogEntries: JogEntry)

@Query("SELECT * FROM jog_entries")
fun getAll(): Flow<List<JogEntry>>

@Query("SELECT * FROM jog_entries WHERE date_time LIKE (:stringDate)")
fun getByDate(stringDate: String): Flow<List<JogEntry>?>
fun getByDate(stringDate: String): Flow<List<JogEntry>>

@Query("SELECT * FROM jog_entries WHERE date_time BETWEEN (:startDate) AND (:endDate)")
fun getByRangeOfDates(startDate: String, endDate: String): Flow<List<JogEntry>>

@Query("SELECT * FROM jog_entries WHERE jog_summary_id IS :jogId")
fun getByID(jogId: Int): Flow<List<JogEntry?>>
fun getByDateRange(startDate: String, endDate: String): Flow<List<JogEntry>>

@Query("DELETE FROM jog_entries")
fun deleteAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@ import javatimefun.zoneddatetime.extensions.getDayDifference
import javatimefun.zoneddatetime.extensions.isAfterDay
import javatimefun.zoneddatetime.extensions.print
import kotlinx.coroutines.flow.Flow
import ramzi.eljabali.justjog.repository.room.jogentries.JogEntry
import ramzi.eljabali.justjog.util.DateFormat
import java.time.ZonedDateTime

@Dao
interface JogSummaryDAO {

@Query("SELECT * FROM jog_entries WHERE jog_summary_id IS :id")
fun getEntriesById(id: Int): Flow<List<JogEntry>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addJogDate(jogSummary: JogSummary)
fun add(jogSummary: JogSummary)

@Query("SELECT * FROM jog_summary")
fun getAll(): Flow<List<JogSummary>>

@Query("SELECT * FROM jog_summary WHERE start_date_time LIKE (:stringDate)")
fun getByDate(stringDate: String): Flow<List<JogSummary>?>
fun getByDate(stringDate: String): Flow<List<JogSummary>>

// @Query("SELECT * FROM jog_summary WHERE CAST(start_date_time AS DATE) BETWEEN CAST(:startDate AS DATE) AND CAST(:endDate AS DATE)")
// fun getByRangeOfDates(startDate: String, endDate: String): Observable<List<JogSummary>>
Expand Down Expand Up @@ -60,8 +65,8 @@ interface JogSummaryDAO {
@Query("DELETE FROM jog_summary")
fun deleteAll()

@Query("SELECT * FROM jog_summary ORDER BY id DESC LIMIT 1")
fun getLast(): Flow<JogSummary?>
@Query("SELECT id FROM jog_summary ORDER BY ID DESC LIMIT 1")
fun getLastID(): Flow<Int?>

@Delete
fun delete(jogSummary: JogSummary)
Expand Down
Loading

0 comments on commit dbe036f

Please sign in to comment.