Skip to content

Commit

Permalink
locker sync job, kmp locker db
Browse files Browse the repository at this point in the history
  • Loading branch information
crc-32 committed Sep 3, 2024
1 parent 6c52f3e commit 2a3e2af
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 4 deletions.
3 changes: 3 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@
<action android:name="android.telecom.InCallService" />
</intent-filter>
</service>
<service android:name=".shared.jobs.AndroidLockerSyncJob"
android:label="Locker Sync Job"
android:permission="android.permission.BIND_JOB_SERVICE" />

<receiver
android:name=".receivers.AppStartReceiver"
Expand Down
3 changes: 3 additions & 0 deletions android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ class MainActivity : FlutterActivity() {
}
}

val jobScheduler = injectionComponent.createAndroidJobScheduler()
jobScheduler.scheduleStartupJobs()

handleIntent(intent)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.rebble.cobble.service.ServiceLifecycleControl
import io.rebble.cobble.shared.database.dao.NotificationChannelDao
import io.rebble.cobble.shared.datastore.KMPPrefs
import io.rebble.cobble.shared.domain.calendar.CalendarSync
import io.rebble.cobble.shared.jobs.AndroidJobScheduler
import io.rebble.libpebblecommon.ProtocolHandler
import io.rebble.libpebblecommon.services.PhoneControlService
import io.rebble.libpebblecommon.services.ProtocolService
Expand Down Expand Up @@ -59,6 +60,7 @@ interface AppComponent {
fun createKMPPrefs(): KMPPrefs
fun createActiveNotifsState(): MutableStateFlow<Map<Uuid, StatusBarNotification>>
fun createNotificationChannelDao(): NotificationChannelDao
fun createAndroidJobScheduler(): AndroidJobScheduler

@Component.Factory
interface Factory {
Expand Down
7 changes: 7 additions & 0 deletions android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import io.rebble.cobble.shared.datastore.KMPPrefs
import io.rebble.cobble.shared.domain.calendar.CalendarSync
import io.rebble.cobble.shared.domain.state.CurrentToken
import io.rebble.cobble.shared.handlers.CalendarActionHandler
import io.rebble.cobble.shared.jobs.AndroidJobScheduler
import io.rebble.libpebblecommon.services.blobdb.BlobDBService
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -79,6 +80,12 @@ abstract class AppModule {
return AppDatabase.instance().notificationChannelDao()
}

@Provides
@Singleton
fun proviceAndroidJobScheduler(context: Context): AndroidJobScheduler {
return KoinPlatformTools.defaultContext().get().get()
}

@Provides
@Singleton
fun provideActiveNotifsState(): MutableStateFlow<Map<UUID, StatusBarNotification>> {
Expand Down
5 changes: 5 additions & 0 deletions android/shared/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.RUN_USER_INITIATED_JOBS" />

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.rebble.cobble.shared.domain.calendar.PlatformCalendarActionExecutor
import io.rebble.cobble.shared.domain.notifications.PlatformNotificationActionExecutor
import io.rebble.cobble.shared.domain.notifications.AndroidNotificationActionExecutor
import io.rebble.cobble.shared.domain.calendar.AndroidCalendarActionExecutor
import io.rebble.cobble.shared.jobs.AndroidJobScheduler
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.koin.dsl.module
Expand All @@ -26,6 +27,7 @@ val androidModule = module {
single(named("activeNotifsState")) {
MutableStateFlow<Map<Uuid, StatusBarNotification>>(emptyMap())
} bind StateFlow::class
single { AndroidJobScheduler() }
singleOf<PlatformNotificationActionExecutor>(::AndroidNotificationActionExecutor)
singleOf<PlatformCalendarActionExecutor>(::AndroidCalendarActionExecutor)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.rebble.cobble.shared.jobs

import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.content.ComponentName
import android.content.Context
import android.os.Build
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.time.Duration.Companion.hours

class AndroidJobScheduler: KoinComponent {
private val context: Context by inject()
private val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler

companion object {
private const val LOCKER_SYNC_JOB_ID = 1
private val LOCKER_SYNC_JOB_PERIOD = 5.hours
}
fun scheduleStartupJobs() {
scheduleLockerSyncPeriodic()
}

private fun buildLockerSyncJob() = JobInfo.Builder(LOCKER_SYNC_JOB_ID, ComponentName(context.applicationContext, AndroidLockerSyncJob::class.java))

fun scheduleLockerSyncPeriodic() {
val jobInfo = buildLockerSyncJob()
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setPeriodic(LOCKER_SYNC_JOB_PERIOD.inWholeMilliseconds)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
jobInfo.setPriority(JobInfo.PRIORITY_LOW)
}

jobScheduler.schedule(jobInfo.build())
}

fun scheduleLockerSync(userInitiated: Boolean) {
val jobInfo = buildLockerSyncJob()
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
if (userInitiated) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
jobInfo.setUserInitiated(true)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
jobInfo.setPriority(JobInfo.PRIORITY_HIGH)
}
}
jobScheduler.schedule(jobInfo.build())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.rebble.cobble.shared.jobs

import android.app.job.JobParameters
import android.app.job.JobService
import android.os.Build
import io.rebble.cobble.shared.Logging
import kotlinx.coroutines.*
import org.koin.core.component.KoinComponent
import kotlin.coroutines.CoroutineContext

class AndroidLockerSyncJob(
private val coroutineContext: CoroutineContext = Dispatchers.Default
): JobService(), KoinComponent {
private lateinit var scope: CoroutineScope
private val lockerSyncJob = LockerSyncJob()

override fun onStartJob(params: JobParameters?): Boolean {
require(params != null) { "Job parameters must not be null" }
scope = CoroutineScope(coroutineContext + makeAndroidJobExceptionHandler(params, this::jobFinished))
scope.launch {
if (lockerSyncJob.beginSync()) {
jobFinished(params, false)
} else {
Logging.i("Locker sync failed, rescheduling")
jobFinished(params, true)
}
}
return true
}

override fun onStopJob(params: JobParameters?): Boolean {
require(params != null) { "Job parameters must not be null" }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
scope.cancel("Job stopped: ${params.stopReason}")
} else {
scope.cancel()
}
return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.rebble.cobble.shared.jobs

import android.app.job.JobParameters
import io.rebble.cobble.shared.Logging
import kotlinx.coroutines.CoroutineExceptionHandler

fun makeAndroidJobExceptionHandler(jobParams: JobParameters, jobFinished: (params: JobParameters, wantsReschedule: Boolean) -> Unit) = CoroutineExceptionHandler { _, throwable ->
Logging.e("Job failed, will reschedule", throwable)
jobFinished(jobParams, true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ import org.koin.mp.KoinPlatformTools
TimelinePin::class,
PersistedNotification::class,
CachedPackageInfo::class,
NotificationChannel::class
NotificationChannel::class,
SyncedLockerEntry::class,
SyncedLockerEntryPlatform::class
],
version = 5,
version = 6,
autoMigrations = [
AutoMigration(1, 2),
AutoMigration(2, 3),
AutoMigration(3, 4),
AutoMigration(4, 5)
AutoMigration(4, 5),
AutoMigration(5, 6)
]
)
@TypeConverters(Converters::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.rebble.cobble.shared.database.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.rebble.cobble.shared.database.NextSyncAction
import io.rebble.cobble.shared.database.entity.SyncedLockerEntry
import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms

@Dao
interface LockerDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrReplace(entry: SyncedLockerEntryWithPlatforms)

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrReplaceAll(entries: List<SyncedLockerEntryWithPlatforms>)

@Query("SELECT * FROM SyncedLockerEntry WHERE id = :id")
suspend fun getEntry(id: String): SyncedLockerEntryWithPlatforms?

@Query("SELECT * FROM SyncedLockerEntry")
suspend fun getAllEntries(): List<SyncedLockerEntryWithPlatforms>

@Query("DELETE FROM SyncedLockerEntryPlatform WHERE lockerEntryId = :entryId")
suspend fun clearPlatformsFor(entryId: String)

@Query("UPDATE SyncedLockerEntry SET nextSyncAction = :action WHERE id = :id")
suspend fun setNextSyncAction(id: String, action: NextSyncAction)

@Query("UPDATE SyncedLockerEntry SET nextSyncAction = :action WHERE id IN (:ids)")
suspend fun setNextSyncAction(ids: Set<String>, action: NextSyncAction)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package io.rebble.cobble.shared.database.entity

import androidx.room.*
import io.rebble.cobble.shared.database.NextSyncAction

@Entity(
indices = [
Index(value = ["uuid"], unique = true),
]
)
data class SyncedLockerEntry(
@PrimaryKey
val id: String,
val uuid: String,
val version: String,
val title: String,
val type: String,
val hearts: Int,
val developerName: String,
val configurable: Boolean,
val timelineEnabled: Boolean,
val removeLink: String,
val shareLink: String,
val pbwLink: String,
val pbwReleaseId: String,
val nextSyncAction: NextSyncAction
)

data class SyncedLockerEntryWithPlatforms(
@Embedded
val entry: SyncedLockerEntry,
@Relation(
parentColumn = "id",
entityColumn = "lockerEntryId"
)
val platforms: List<SyncedLockerEntryPlatform>
)

@Entity(
foreignKeys = [
androidx.room.ForeignKey(
entity = SyncedLockerEntry::class,
parentColumns = ["id"],
childColumns = ["lockerEntryId"],
onDelete = androidx.room.ForeignKey.CASCADE
)
],
indices = [
Index(value = ["lockerEntryId"]),
]
)
data class SyncedLockerEntryPlatform(
@PrimaryKey(autoGenerate = true)
val platformEntryId: Int,
val lockerEntryId: String,
val sdkVersion: String,
val processInfoFlags: Int,
val name: String,
val description: String,
val icon: String,
)

/**
* Compare the data of this [SyncedLockerEntry] with another [SyncedLockerEntry] ignoring auto-generated fields.
*/
fun SyncedLockerEntryWithPlatforms.dataEqualTo(other: SyncedLockerEntryWithPlatforms): Boolean {
return entry == other.entry &&
platforms.all { platform ->
other.platforms.any { it.dataEqualTo(platform) }
}
}

/**
* Compare the data of this [SyncedLockerEntryPlatform] with another [SyncedLockerEntryPlatform] ignoring auto-generated fields.
*/
fun SyncedLockerEntryPlatform.dataEqualTo(other: SyncedLockerEntryPlatform): Boolean {
return lockerEntryId == other.lockerEntryId &&
sdkVersion == other.sdkVersion &&
processInfoFlags == other.processInfoFlags &&
name == other.name &&
description == other.description &&
icon == other.icon
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.rebble.cobble.shared.domain.api.appstore

import io.rebble.cobble.shared.database.NextSyncAction
import io.rebble.cobble.shared.database.entity.SyncedLockerEntry
import io.rebble.cobble.shared.database.entity.SyncedLockerEntryPlatform
import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName

Expand Down Expand Up @@ -98,4 +102,41 @@ data class LockerEntryPBW(
val file: String,
@SerialName("icon_resource_id") val iconResourceId: Int,
@SerialName("release_id") val releaseId: String
)
)

fun LockerEntry.toEntity(): SyncedLockerEntryWithPlatforms {
check(hardwarePlatforms.isNotEmpty()) { "Hardware platforms are empty" }
return SyncedLockerEntryWithPlatforms(
entry = SyncedLockerEntry(
id = id,
uuid = uuid,
version = version ?: error("Version is null"),
title = title,
type = type,
hearts = hearts,
developerName = developer.name,
configurable = isConfigurable,
timelineEnabled = isTimelineEnabled,
removeLink = links.remove,
shareLink = links.share,
pbwLink = pbw?.file ?: error("PBW is null"),
pbwReleaseId = pbw.releaseId,
nextSyncAction = NextSyncAction.Upload
),
platforms = hardwarePlatforms.map {
it.toEntity(id)
},
)
}

fun LockerEntryPlatform.toEntity(lockerEntryId: String): SyncedLockerEntryPlatform {
return SyncedLockerEntryPlatform(
platformEntryId = 0,
lockerEntryId = lockerEntryId,
sdkVersion = sdkVersion,
processInfoFlags = pebbleProcessInfoFlags,
name = name,
description = description,
icon = images.icon
)
}
Loading

0 comments on commit 2a3e2af

Please sign in to comment.