Skip to content

Commit

Permalink
kmp locker clear debug tool, fix locker syncing watchfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
crc-32 committed Sep 20, 2024
1 parent 9c1f917 commit 58ad9d6
Show file tree
Hide file tree
Showing 18 changed files with 186 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.content.getSystemService
import io.rebble.cobble.shared.util.NotificationId
import javax.inject.Inject

class NotificationChannelManager @Inject constructor(context: Context) {
Expand All @@ -14,31 +15,35 @@ class NotificationChannelManager @Inject constructor(context: Context) {

notificationManager.createNotificationChannel(
NotificationChannel(
NOTIFICATION_CHANNEL_WATCH_CONNECTED,
NotificationId.NOTIFICATION_CHANNEL_WATCH_CONNECTED,
context.getString(R.string.connected),
NotificationManager.IMPORTANCE_LOW
)
)

notificationManager.createNotificationChannel(
NotificationChannel(
NOTIFICATION_CHANNEL_WATCH_CONNECTING,
NotificationId.NOTIFICATION_CHANNEL_WATCH_CONNECTING,
context.getString(R.string.connecting),
NotificationManager.IMPORTANCE_LOW
)
)

notificationManager.createNotificationChannel(
NotificationChannel(
NOTIFICATION_CHANNEL_WARNINGS,
NotificationId.NOTIFICATION_CHANNEL_WARNINGS,
context.getString(R.string.warnings),
NotificationManager.IMPORTANCE_DEFAULT
)
)

notificationManager.createNotificationChannel(
NotificationChannel(
NotificationId.NOTIFICATION_CHANNEL_JOBS,
context.getString(R.string.jobs),
NotificationManager.IMPORTANCE_MIN
)
)
}
}
}

val NOTIFICATION_CHANNEL_WATCH_CONNECTED = "WATCH_CONNECTED"
val NOTIFICATION_CHANNEL_WATCH_CONNECTING = "WATCH_CONNECTING"
val NOTIFICATION_CHANNEL_WARNINGS = "WARNINGS"
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.rebble.cobble.bluetooth.EmulatedPebbleDevice
import io.rebble.cobble.shared.handlers.CobbleHandler
import io.rebble.cobble.shared.domain.calendar.CalendarSync
import io.rebble.cobble.shared.domain.state.ConnectionState
import io.rebble.cobble.shared.util.NotificationId
import io.rebble.libpebblecommon.ProtocolHandler
import io.rebble.libpebblecommon.services.notification.NotificationService
import kotlinx.coroutines.*
Expand All @@ -37,11 +38,11 @@ class WatchService : LifecycleService() {
private lateinit var mainNotifBuilder: NotificationCompat.Builder

override fun onCreate() {
mainNotifBuilder = createBaseNotificationBuilder(NOTIFICATION_CHANNEL_WATCH_CONNECTING)
mainNotifBuilder = createBaseNotificationBuilder(NotificationId.NOTIFICATION_CHANNEL_WATCH_CONNECTING)
.setContentTitle("Waiting to connect")
.setContentText(null)
.setSmallIcon(R.drawable.ic_notification_disconnected)
startForeground(1, mainNotifBuilder.build())
startForeground(NotificationId.WATCH_CONNECTION, mainNotifBuilder.build())

val injectionComponent = (applicationContext as CobbleApplication).component
val serviceComponent = injectionComponent.createServiceSubcomponentFactory()
Expand Down Expand Up @@ -92,28 +93,28 @@ class WatchService : LifecycleService() {
icon = R.drawable.ic_notification_disconnected
titleText = "Connecting"
deviceName = null
channel = NOTIFICATION_CHANNEL_WATCH_CONNECTING
channel = NotificationId.NOTIFICATION_CHANNEL_WATCH_CONNECTING
}

is ConnectionState.WaitingForTransport -> {
icon = R.drawable.ic_notification_disconnected
titleText = getString(R.string.bluetooth_off)
deviceName = null
channel = NOTIFICATION_CHANNEL_WATCH_CONNECTING
channel = NotificationId.NOTIFICATION_CHANNEL_WATCH_CONNECTING
}

is ConnectionState.Connected -> {
icon = R.drawable.ic_notification_connected
titleText = "Connected to device"
deviceName = if (it.watch is EmulatedPebbleDevice) "[EMU] ${it.watch.address}" else if (it.watch is BluetoothPebbleDevice) (it.watch as BluetoothPebbleDevice).bluetoothDevice.name!! else it.watch.address
channel = NOTIFICATION_CHANNEL_WATCH_CONNECTED
channel = NotificationId.NOTIFICATION_CHANNEL_WATCH_CONNECTED
}

is ConnectionState.RecoveryMode -> {
icon = R.drawable.ic_notification_connected
titleText = "Connected to device (Recovery Mode)"
deviceName = if (it.watch is EmulatedPebbleDevice) "[EMU] ${it.watch.address}" else if (it.watch is BluetoothPebbleDevice) (it.watch as BluetoothPebbleDevice).bluetoothDevice.name!! else it.watch.address
channel = NOTIFICATION_CHANNEL_WATCH_CONNECTED
channel = NotificationId.NOTIFICATION_CHANNEL_WATCH_CONNECTED
}
else -> error("Unhandled connection state")
}
Expand Down
1 change: 1 addition & 0 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
<string name="connecting">Watch connecting</string>
<string name="bluetooth_off">Bluetooth is off</string>
<string name="warnings">Warnings</string>
<string name="jobs">Background Jobs</string>
</resources>
2 changes: 1 addition & 1 deletion android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ kotlin = "2.0.20-Beta2"
kotlinxDatetime = "0.6.0"
kotlinxSerializationJson = "1.7.1"
ksp = "2.0.20-Beta2-1.0.23"
libpebblecommonVersion = "0.1.23"
libpebblecommonVersion = "0.1.24"
errorproneVersion = "2.26.1"
spotbugsVersion = "4.8.6"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package io.rebble.cobble.shared.jobs
import android.app.job.JobParameters
import android.app.job.JobService
import android.os.Build
import androidx.core.app.NotificationCompat
import io.rebble.cobble.shared.Logging
import io.rebble.cobble.shared.util.NotificationId
import kotlinx.coroutines.*
import org.koin.core.component.KoinComponent
import kotlin.coroutines.CoroutineContext
Expand All @@ -17,6 +19,14 @@ class AndroidLockerSyncJob(
override fun onStartJob(params: JobParameters?): Boolean {
require(params != null) { "Job parameters must not be null" }
scope = CoroutineScope(coroutineContext + makeAndroidJobExceptionHandler(params, this::jobFinished))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && params.isUserInitiatedJob) {
val notif = NotificationCompat.Builder(applicationContext, NotificationId.NOTIFICATION_CHANNEL_JOBS)
.setContentTitle("Syncing apps and watchfaces")
.setSmallIcon(android.R.drawable.ic_popup_sync)
.setOngoing(true)
.build()
setNotification(params, NotificationId.LOCKER_SYNC, notif, JOB_END_NOTIFICATION_POLICY_REMOVE)
}
scope.launch {
if (lockerSyncJob.beginSync()) {
jobFinished(params, false)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.rebble.cobble.shared.util

object NotificationId {
const val WATCH_CONNECTION = 1
const val LOCKER_SYNC = 2

const val NOTIFICATION_CHANNEL_WATCH_CONNECTED = "WATCH_CONNECTED"
const val NOTIFICATION_CHANNEL_WATCH_CONNECTING = "WATCH_CONNECTING"
const val NOTIFICATION_CHANNEL_WARNINGS = "WARNINGS"
const val NOTIFICATION_CHANNEL_JOBS = "JOBS"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.rebble.libpebblecommon.packets.blobdb.TimelineItem
import io.rebble.libpebblecommon.structmapper.SUInt
import io.rebble.libpebblecommon.structmapper.StructMapper
import io.rebble.libpebblecommon.util.DataBuffer
import io.rebble.libpebblecommon.util.Endian
import io.rebble.libpebblecommon.util.encodeToByteArrayTrimmed
import kotlinx.serialization.Serializable

Expand Down Expand Up @@ -38,7 +39,7 @@ data class TimelineAttribute(
}

uint32 != null -> {
SUInt(StructMapper(), uint32.toUInt(), '<').toBytes()
SUInt(StructMapper(), uint32.toUInt(), Endian.Little).toBytes()
}

else -> throw IllegalArgumentException("Received empty timeline attribute: $this")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface LockerDao {
suspend fun setNextSyncAction(ids: Set<String>, action: NextSyncAction)

@Transaction
@Query("SELECT * FROM SyncedLockerEntry WHERE nextSyncAction in (1, 2)")
@Query("SELECT * FROM SyncedLockerEntry WHERE nextSyncAction in ('Upload', 'Delete')")
suspend fun getEntriesForSync(): List<SyncedLockerEntryWithPlatforms>

@Transaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,23 @@ import io.rebble.libpebblecommon.packets.blobdb.BlobResponse
import io.rebble.libpebblecommon.services.blobdb.BlobDBService
import io.rebble.libpebblecommon.structmapper.SUUID
import io.rebble.libpebblecommon.structmapper.StructMapper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.random.Random

class LockerSyncJob: KoinComponent {
private val lockerDao: LockerDao by inject()
suspend fun beginSync(): Boolean {
val locker = RWS.appstoreClient?.getLocker() ?: return false
val storedLocker = lockerDao.getAllEntries()
val locker = withContext(Dispatchers.IO) {
RWS.appstoreClient?.getLocker()
} ?: return false
val storedLocker = withContext(Dispatchers.IO) {
lockerDao.getAllEntries()
}

val changedEntries = locker.filter { new ->
val newPlat = new.hardwarePlatforms.map { it.toEntity(new.id) }
Expand Down Expand Up @@ -64,69 +71,72 @@ class LockerSyncJob: KoinComponent {
.fromProtocolNumber(connectedWatch.metadata.value?.running?.hardwarePlatform?.get() ?: 0u)
connectedWatchType?.let {
val blobDBService = connectedWatch.blobDBService
entries.forEach { row ->
val entry = row.entry
val platformName = AppCompatibility.getBestVariant(
connectedWatchType.watchType,
row.platforms.map { plt -> plt.name }
)?.codename
val platform = row.platforms.firstOrNull { plt -> plt.name == platformName }
val res = platform?.let {
when (entry.nextSyncAction) {
NextSyncAction.Upload -> {
val appVersionMajor = entry.version.split(".").getOrNull(0)?.toUByte() ?: 0u
val appVersionMinor = entry.version.split(".").getOrNull(1)?.toUByte() ?: 0u
val sdkVersionMajor = platform.sdkVersion.split(".")[0].toUByte()
val sdkVersionMinor = platform.sdkVersion.split(".")[1].toUByte()
return@let blobDBService.send(
BlobCommand.InsertCommand(
Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(),
BlobCommand.BlobDatabase.App,
SUUID(StructMapper(), uuidFrom(entry.uuid)).toBytes(),
AppMetadata().also { meta ->
meta.uuid.set(uuidFrom(entry.uuid))
meta.flags.set(platform.processInfoFlags.toUInt())
meta.icon.set(0u)
meta.appVersionMajor.set(appVersionMajor)
meta.appVersionMinor.set(appVersionMinor)
meta.sdkVersionMajor.set(sdkVersionMajor)
meta.sdkVersionMinor.set(sdkVersionMinor)
meta.appName.set(entry.title)
}.toBytes()
)
)
return withContext(Dispatchers.IO) {
entries.forEach { row ->
val entry = row.entry
val platformName = AppCompatibility.getBestVariant(
connectedWatchType.watchType,
row.platforms.map { plt -> plt.name }
)?.codename
val platform = row.platforms.firstOrNull { plt -> plt.name == platformName }
val res = platform?.let {
when (entry.nextSyncAction) {
NextSyncAction.Upload -> {
val versionCode = Regex("""\d+\.\d+""").find(entry.version)?.value ?: "0.0"
val appVersionMajor = versionCode.split(".")[0].toUByte()
val appVersionMinor = versionCode.split(".")[1].toUByte()
val sdkVersionMajor = platform.sdkVersion.split(".")[0].toUByte()
val sdkVersionMinor = platform.sdkVersion.split(".")[1].toUByte()
val packet = BlobCommand.InsertCommand(
Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(),
BlobCommand.BlobDatabase.App,
SUUID(StructMapper(), uuidFrom(entry.uuid)).toBytes(),
AppMetadata().also { meta ->
meta.uuid.set(uuidFrom(entry.uuid))
meta.flags.set(platform.processInfoFlags.toUInt())
meta.icon.set(0u)
meta.appVersionMajor.set(appVersionMajor)
meta.appVersionMinor.set(appVersionMinor)
meta.sdkVersionMajor.set(sdkVersionMajor)
meta.sdkVersionMinor.set(sdkVersionMinor)
meta.appName.set(entry.title)
}.toBytes()
)
return@let blobDBService.send(packet)
}
NextSyncAction.Delete -> {
return@let blobDBService.send(
BlobCommand.DeleteCommand(
Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(),
BlobCommand.BlobDatabase.App,
SUUID(StructMapper(), uuidFrom(entry.uuid)).toBytes()
)
)
}
else -> {
Logging.w("Unknown next sync action ${entry.nextSyncAction}")
return@let null
}
}
}
when (res?.responseValue) {
BlobResponse.BlobStatus.Success -> {
lockerDao.setNextSyncAction(entry.id, NextSyncAction.Nothing)
}
NextSyncAction.Delete -> {
return@let blobDBService.send(
BlobCommand.DeleteCommand(
Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(),
BlobCommand.BlobDatabase.App,
SUUID(StructMapper(), uuidFrom(entry.uuid)).toBytes()
)
)
BlobResponse.BlobStatus.DatabaseFull -> {
Logging.w("Database full, stopping sync")
return@withContext true
}
BlobResponse.BlobStatus.WatchDisconnected -> {
Logging.w("Watch disconnected, stopping sync")
return@withContext false
}
else -> {
Logging.w("Unknown next sync action ${entry.nextSyncAction}")
return@let null
Logging.w("Failed to sync app ${entry.id}: ${res?.responseValue}")
}
}
}
when (res?.responseValue) {
BlobResponse.BlobStatus.Success -> {
lockerDao.setNextSyncAction(entry.id, NextSyncAction.Nothing)
}
BlobResponse.BlobStatus.DatabaseFull -> {
Logging.w("Database full, stopping sync")
return true
}
BlobResponse.BlobStatus.WatchDisconnected -> {
Logging.w("Watch disconnected, stopping sync")
return false
}
else -> {
Logging.w("Failed to sync app ${entry.id}: ${res?.responseValue}")
}
}
return@withContext true
}
} ?: run {
Logging.w("Unknown watch type")
Expand All @@ -136,7 +146,6 @@ class LockerSyncJob: KoinComponent {
Logging.w("No connected watch to sync to")
return false
}
return true
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ object Routes {
object Home {
const val LOCKER_APPS = "locker_apps"
const val LOCKER_WATCHFACES = "locker_watchfaces"
const val TEST_PAGE = "test_page"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ fun MainView(navController: NavHostController = rememberNavController()) {
composable(Routes.Home.LOCKER_APPS) {
HomeScaffold(HomePage.Locker(LockerTabs.Apps), onNavChange = navController::navigate)
}
composable(Routes.Home.TEST_PAGE) {
HomeScaffold(HomePage.TestPage, onNavChange = navController::navigate)
}
}
}
}
Expand Down
Loading

0 comments on commit 58ad9d6

Please sign in to comment.