Skip to content

Commit

Permalink
Perform daily sync in background
Browse files Browse the repository at this point in the history
  • Loading branch information
bubelov committed Apr 18, 2024
1 parent a76927b commit 616471e
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

- Perform daily sync in background

## [0.7.1] - 2024-03-17

- Fix issue with deleted places not being shown in a change log
Expand Down
7 changes: 7 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ dependencies {
implementation("io.coil-kt:coil:$coilVer")
implementation("io.coil-kt:coil-svg:$coilVer")

// Background job scheduler
// Used to fetch new data in background
// https://developer.android.com/jetpack/androidx/releases/work
val workVer = "2.9.0"
implementation("androidx.work:work-runtime-ktx:$workVer")
androidTestImplementation("androidx.work:work-testing:$workVer")

// Common test dependencies
// https://junit.org/junit4/
val junitVer = "4.13.2"
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- Optional, local activity notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:name="app.App"
android:allowBackup="false"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/kotlin/app/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.SvgDecoder
import db.persistentDatabase
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koin.dsl.module
import sync.BackgroundSyncScheduler

class App : Application(), ImageLoaderFactory {

Expand All @@ -22,6 +24,8 @@ class App : Application(), ImageLoaderFactory {
module { single { persistentDatabase(this@App) } },
)
}

get<BackgroundSyncScheduler>().schedule()
}

override fun newImageLoader(): ImageLoader {
Expand Down
18 changes: 14 additions & 4 deletions app/src/main/kotlin/app/AppModule.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app

import android.content.Context
import api.Api
import api.ApiImpl
import area.AreaQueries
Expand Down Expand Up @@ -27,6 +28,8 @@ import reports.ReportsRepo
import search.SearchModel
import search.SearchResultModel
import sync.Sync
import sync.BackgroundSyncScheduler
import sync.SyncNotificationController
import user.UserQueries
import user.UsersModel
import user.UsersRepo
Expand All @@ -39,10 +42,14 @@ val appModule = module {
single {
OkHttpClient.Builder()
.addInterceptor(BrotliInterceptor)
// .addInterceptor {
// android.util.Log.d("okhttp", it.request().url.toString())
// it.proceed(it.request())
// }
.apply {
if (get<Context>().isDebuggable()) {
addInterceptor {
android.util.Log.d("okhttp", it.request().url.toString())
it.proceed(it.request())
}
}
}
.build()
}

Expand All @@ -53,6 +60,9 @@ val appModule = module {
)
}.bind(Api::class)

singleOf(::BackgroundSyncScheduler)
singleOf(::SyncNotificationController)

singleOf(::AreaQueries)
singleOf(::AreasRepo)
viewModelOf(::AreasModel)
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/kotlin/app/ContextExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app

import android.content.Context
import android.content.pm.ApplicationInfo

fun Context.isDebuggable(): Boolean {
return applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
}
12 changes: 12 additions & 0 deletions app/src/main/kotlin/map/MapFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Paint
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
Expand Down Expand Up @@ -97,6 +98,10 @@ class MapFragment : Fragment() {
}
}

private val postNotificationsPermissionRequest = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
) { }

private var emptyClusterBitmap: Bitmap? = null

override fun onCreateView(
Expand Down Expand Up @@ -298,6 +303,7 @@ class MapFragment : Fragment() {
visibleElements += marker
binding.map.overlays += marker
}

is MapModel.MapItem.Meetup -> {
val marker = Marker(binding.map)
marker.position = GeoPoint(it.meetup.lat, it.meetup.lon)
Expand Down Expand Up @@ -432,6 +438,12 @@ class MapFragment : Fragment() {
}
}
}

if (Build.VERSION.SDK_INT >= 33) {
postNotificationsPermissionRequest.launch(
arrayOf(Manifest.permission.POST_NOTIFICATIONS)
)
}
}

override fun onDestroyView() {
Expand Down
46 changes: 46 additions & 0 deletions app/src/main/kotlin/sync/BackgroundSyncScheduler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package sync

import android.content.Context
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import app.isDebuggable
import java.util.concurrent.TimeUnit

class BackgroundSyncScheduler(private val context: Context) {
fun schedule() {
val workManager = WorkManager.getInstance(context)

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.build()

val periodicSyncRequest = if (context.isDebuggable()) {
PeriodicWorkRequestBuilder<SyncWorker>(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.HOURS,
)
.setConstraints(constraints)
.setInitialDelay(1, TimeUnit.HOURS)
} else {
PeriodicWorkRequestBuilder<SyncWorker>(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.DAYS,
)
.setConstraints(constraints)
.setInitialDelay(1, TimeUnit.DAYS)
}.build()

workManager.enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
periodicSyncRequest,
)
}

companion object {
private const val WORK_NAME = "sync"
}
}
9 changes: 8 additions & 1 deletion app/src/main/kotlin/sync/Sync.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Sync(
private val usersRepo: UsersRepo,
private val eventsRepo: EventsRepo,
private val logRecordQueries: LogRecordQueries,
private val syncNotificationController: SyncNotificationController,
) {

private val _active = MutableStateFlow(false)
Expand All @@ -48,6 +49,8 @@ class Sync(
return
}

var elementsSyncReport: ElementsRepo.SyncReport? = null

runCatching {
withContext(Dispatchers.IO) {
logRecordQueries.insert(JSONObject(mapOf("message" to "started sync")))
Expand All @@ -59,7 +62,10 @@ class Sync(

withContext(Dispatchers.IO) {
listOf(
async { Log.d(TAG, elementsRepo.sync().getOrThrow().toString()) },
async {
elementsSyncReport = elementsRepo.sync().getOrThrow()
Log.d(TAG, elementsSyncReport.toString())
},
async { Log.d(TAG, reportsRepo.sync().getOrThrow().toString()) },
async { Log.d(TAG, areasRepo.sync().getOrThrow().toString()) },
async { Log.d(TAG, usersRepo.sync().getOrThrow().toString()) },
Expand All @@ -69,6 +75,7 @@ class Sync(
}.onSuccess {
Log.d(TAG, "Finished sync in ${System.currentTimeMillis() - startTime} ms")
confRepo.update { it.copy(lastSyncDate = ZonedDateTime.now(ZoneOffset.UTC)) }
syncNotificationController.showPostSyncNotifications(elementsSyncReport)
_active.update { false }
}.onFailure {
Log.e(TAG, "Sync failed", it)
Expand Down
85 changes: 85 additions & 0 deletions app/src/main/kotlin/sync/SyncNotificationController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package sync

import android.Manifest
import android.app.Activity
import android.app.NotificationChannel
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.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import app.isDebuggable
import element.ElementsRepo
import org.btcmap.R

class SyncNotificationController(private val context: Context) {

fun showPostSyncNotifications(elementsSyncReport: ElementsRepo.SyncReport?) {
if (elementsSyncReport == null) {
return
}

if (context.isDebuggable()) {
createSyncSummaryNotificationChannel(context)
}

createNewMerchantsNotificationChannel(context)

val intent = Intent(context, Activity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}

val pendingIntent =
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)

val builder = NotificationCompat.Builder(context, SYNC_SUMMARY_CHANNEL_ID)
.setSmallIcon(R.drawable.area_placeholder_icon)
.setContentTitle(context.getString(R.string.app_name))
.setContentText(elementsSyncReport.toString())
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)

if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS,
) == PackageManager.PERMISSION_GRANTED
) {
with(NotificationManagerCompat.from(context)) {
notify(SYNC_SUMMARY_NOTIFICATION_ID, builder.build())
}
}
}

private fun createSyncSummaryNotificationChannel(context: Context) {
val name = "Sync summary"
val descriptionText = "Sync summary"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(SYNC_SUMMARY_CHANNEL_ID, name, importance).apply {
description = descriptionText
}
val notificationManager = context.getSystemService<NotificationManager>()!!
notificationManager.createNotificationChannel(channel)
}

private fun createNewMerchantsNotificationChannel(context: Context) {
val name = "New merchants"
val descriptionText = "New merchants"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(NEW_MERCHANTS_CHANNEL_ID, name, importance).apply {
description = descriptionText
}
val notificationManager = context.getSystemService<NotificationManager>()!!
notificationManager.createNotificationChannel(channel)
}

companion object {
private const val SYNC_SUMMARY_CHANNEL_ID = "sync_summary"
private const val SYNC_SUMMARY_NOTIFICATION_ID = 1
private const val NEW_MERCHANTS_CHANNEL_ID = "new_merchants"
}
}
27 changes: 27 additions & 0 deletions app/src/main/kotlin/sync/SyncWorker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package sync

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import app.App
import conf.ConfRepo
import kotlinx.coroutines.runBlocking
import org.koin.android.ext.android.get

class SyncWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork() = runBlocking { doWorkAsync() }

private suspend fun doWorkAsync(): Result {
val app = applicationContext as App
val conf = app.get<ConfRepo>().conf.value
val sync = app.get<Sync>()

if (conf.lastSyncDate == null) {
return Result.retry()
}

sync.sync()

return Result.success()
}
}

0 comments on commit 616471e

Please sign in to comment.