From 622b82b028f742d6f99252c53f3c1ddfe483a88d Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 9 Jul 2024 12:29:46 +0100 Subject: [PATCH 01/20] begin pkjs webview implementation --- android/app/build.gradle | 0 .../cobble/shared/js/WebViewJsRunnerTest.kt | 62 +++++ android/gradle/libs.versions.toml | 6 + android/shared/build.gradle.kts | 2 +- .../androidMain/assets/webview_startup.html | 10 + .../src/androidMain/assets/webview_startup.js | 1 + .../cobble/shared/js/WebViewJsRunner.kt | 220 ++++++++++++++++++ .../cobble/shared/js/WebViewPKJSInterface.kt | 35 +++ .../shared/js/WebViewPrivatePKJSInterface.kt | 40 ++++ .../io/rebble/cobble/shared/js/JsRunner.kt | 8 + .../rebble/cobble/shared/js/PKJSInterface.kt | 27 +++ .../cobble/shared/js/PrivatePKJSInterface.kt | 8 + 12 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 android/app/build.gradle create mode 100644 android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt create mode 100644 android/shared/src/androidMain/assets/webview_startup.html create mode 100644 android/shared/src/androidMain/assets/webview_startup.js create mode 100644 android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt create mode 100644 android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt create mode 100644 android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSInterface.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PrivatePKJSInterface.kt diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 00000000..e69de29b diff --git a/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt b/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt new file mode 100644 index 00000000..b21ac075 --- /dev/null +++ b/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt @@ -0,0 +1,62 @@ +package io.rebble.cobble.shared.js + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo +import io.rebble.libpebblecommon.util.runBlocking +import kotlinx.coroutines.delay +import kotlinx.serialization.json.Json +import org.junit.Before +import org.junit.Test + +class WebViewJsRunnerTest { + + private lateinit var context: Context + private val json = Json {ignoreUnknownKeys = true} + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + } + @Test + fun test() = runBlocking { + val appInfo: PbwAppInfo = json.decodeFromString( + """ + { + "targetPlatforms": [ + "aplite", + "basalt", + "chalk", + "diorite" + ], + "projectType": "native", + "messageKeys": {}, + "companyName": "ttmm", + "enableMultiJS": true, + "versionLabel": "2.12", + "longName": "ttmmbrn", + "shortName": "ttmmbrn", + "name": "ttmmbrn", + "sdkVersion": "3", + "displayName": "ttmmbrn", + "uuid": "c4c60c62-2c22-4ad7-aef4-cad9481da58b", + "appKeys": {}, + "capabilities": [ + "health", + "location", + "configurable" + ], + "watchapp": { + "watchface": true + }, + "resources": { + "media": [] + } + } + """.trimIndent() + ) + val printTestPath = "file:///android_asset/print_test.js" + val webViewJsRunner = WebViewJsRunner(context, appInfo, printTestPath) + webViewJsRunner.start() + delay(5000) + } +} \ No newline at end of file diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 3e507c95..ab5d0314 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -14,9 +14,11 @@ libpebblecommonVersion = "0.1.24" errorproneVersion = "2.26.1" spotbugsVersion = "4.8.6" +protoliteWellKnownTypes = "18.0.0" room = "2.7.0-alpha08" room-sqlite = "2.5.0-alpha08" datastore = "1.1.1" +androidxTest = "1.6.1" uuidVersion = "0.8.4" compose-lib = "1.7.0-beta02" compose-nav = "2.7.0-alpha07" @@ -41,6 +43,9 @@ jetbrains-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", versi [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxTest" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTest" } +androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidxTest" } dagger = { module = "com.google.dagger:dagger", version.ref = "daggerVersion" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "daggerVersion" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } @@ -66,6 +71,7 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationJson" } libpebblecommon = { module = "io.rebble.libpebblecommon:libpebblecommon", version.ref = "libpebblecommonVersion" } uuid = { module = "com.benasher44:uuid", version.ref = "uuidVersion" } +protolite-wellknowntypes = { module = "com.google.firebase:protolite-well-known-types", version.ref = "protoliteWellKnownTypes" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorVersion" } ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktorVersion" } diff --git a/android/shared/build.gradle.kts b/android/shared/build.gradle.kts index 46b06927..1a0f6d36 100644 --- a/android/shared/build.gradle.kts +++ b/android/shared/build.gradle.kts @@ -99,7 +99,7 @@ android { } } dependencies { - implementation("com.google.firebase:protolite-well-known-types:18.0.0") + implementation(libs.protolite.wellknowntypes) add("kspCommonMainMetadata", libs.androidx.room.compiler) add("kspAndroid", libs.androidx.room.compiler) } diff --git a/android/shared/src/androidMain/assets/webview_startup.html b/android/shared/src/androidMain/assets/webview_startup.html new file mode 100644 index 00000000..7ddf7933 --- /dev/null +++ b/android/shared/src/androidMain/assets/webview_startup.html @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/shared/src/androidMain/assets/webview_startup.js b/android/shared/src/androidMain/assets/webview_startup.js new file mode 100644 index 00000000..027adb8c --- /dev/null +++ b/android/shared/src/androidMain/assets/webview_startup.js @@ -0,0 +1 @@ +/** @namespace Pebble */ \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt new file mode 100644 index 00000000..49d163a3 --- /dev/null +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt @@ -0,0 +1,220 @@ +package io.rebble.cobble.shared.js + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.net.http.SslError +import android.os.Message +import android.view.View +import android.webkit.* +import io.rebble.cobble.shared.Logging +import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class WebViewJsRunner(val context: Context, private val scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner(appInfo, jsPath) { + + companion object { + const val API_NAMESPACE = "Pebble" + const val PRIVATE_API_NAMESPACE = "_$API_NAMESPACE" + const val STARTUP_URL = "file:///android_asset/webview_startup.html" + } + + private var webView: WebView? = null + private val initializedLock = Object() + private val publicJsInterface = WebViewPKJSInterface() + private val privateJsInterface = WebViewPrivatePKJSInterface(this, scope) + private val interfaces = setOf( + Pair(API_NAMESPACE, publicJsInterface), + Pair(PRIVATE_API_NAMESPACE, privateJsInterface) + ) + + private val webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + return true + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + Logging.d("Page finished loading: $url") + } + + override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { + super.onReceivedError(view, request, error) + Logging.e("Error loading page: ${error?.errorCode} ${error?.description}") + } + + override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) { + super.onReceivedSslError(view, handler, error) + Logging.e("SSL error loading page: ${error?.primaryError}") + handler?.cancel() + } + + override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? { + if (isForbidden(request?.url)) { + return object : WebResourceResponse("text/plain", "utf-8", null) { + override fun getStatusCode(): Int { + return 403 + } + + override fun getReasonPhrase(): String { + return "Forbidden" + } + } + } else { + return super.shouldInterceptRequest(view, request) + } + } + } + + private fun isForbidden(url: Uri?): Boolean { + return if (url == null) { + Logging.w("Blocking null URL") + true + } else if (url.scheme?.uppercase() != "FILE") { + false + } else if (url.path?.uppercase() == jsPath.uppercase()) { + false + } else { + Logging.w("Blocking access to file: ${url.path}") + true + } + } + + private val chromeClient = object : WebChromeClient() { + override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { + //TODO: forward to developer log + consoleMessage?.let { + Logging.d("WebView: ${it.message()}") + } + return false + } + + override fun onCreateWindow(view: WebView?, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message?): Boolean { + return false + } + + override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { + return false + } + + override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { + return false + } + + override fun onJsBeforeUnload(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { + return false + } + + override fun onJsPrompt(view: WebView?, url: String?, message: String?, defaultValue: String?, result: JsPromptResult?): Boolean { + return false + } + + override fun onShowCustomView(view: View?, callback: CustomViewCallback?) { + //Stub + } + + override fun onShowFileChooser(webView: WebView?, filePathCallback: ValueCallback>?, fileChooserParams: FileChooserParams?): Boolean { + return false + } + + override fun onGeolocationPermissionsShowPrompt(origin: String?, callback: GeolocationPermissions.Callback?) { + Logging.d("Geolocation permission requested for $origin") + callback?.invoke(origin, true, false) + } + + override fun onPermissionRequest(request: PermissionRequest?) { + request?.deny() + } + } + + @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") + private suspend fun init() = withContext(Dispatchers.Main) { + webView = WebView(context).also { + it.setWillNotDraw(true) + val settings = it.settings + settings.javaScriptEnabled = true + settings.allowFileAccess = true + settings.allowContentAccess = false + + //TODO: use WebViewAssetLoader instead + settings.allowUniversalAccessFromFileURLs = true + settings.allowFileAccessFromFileURLs = true + + settings.databaseEnabled = true + settings.domStorageEnabled = true + settings.cacheMode = WebSettings.LOAD_NO_CACHE + it.clearCache(true) + + interfaces.forEach { (namespace, jsInterface) -> + it.addJavascriptInterface(jsInterface, namespace) + } + webView?.webViewClient = webViewClient + webView?.webChromeClient = chromeClient + } + } + + + override suspend fun start() { + synchronized(initializedLock) { + check(webView == null) { "WebviewJsRunner already started" } + } + try { + init() + } catch (e: Exception) { + synchronized(initializedLock) { + webView = null + } + throw e + } + check(webView != null) { "WebView not initialized" } + loadApp(jsPath) + } + + override suspend fun stop() { + //TODO: Close config screens + + withContext(Dispatchers.Main) { + interfaces.forEach { (namespace, _) -> + webView?.removeJavascriptInterface(namespace) + } + webView?.loadUrl("about:blank") + webView?.stopLoading() + webView?.clearHistory() + webView?.removeAllViews() + webView?.clearCache(true) + webView?.destroy() + } + synchronized(initializedLock) { + webView = null + } + } + + private suspend fun loadApp(url: String) { + check(webView != null) { "WebView not initialized" } + withContext(Dispatchers.Main) { + webView?.loadUrl( + Uri.parse(STARTUP_URL).buildUpon() + .appendQueryParameter("params", "{\"loadUrl\": \"$url\"}") + .build() + .toString() + ) + } + } + + suspend fun loadAppJs(params: String?) { + check(webView != null) { "WebView not initialized" } + if (params == null) { + Logging.e("No params passed to loadAppJs") + return + } + + + } +} \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt new file mode 100644 index 00000000..46e809ec --- /dev/null +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt @@ -0,0 +1,35 @@ +package io.rebble.cobble.shared.js + +import android.webkit.JavascriptInterface + +class WebViewPKJSInterface: PKJSInterface { + @JavascriptInterface + override fun showSimpleNotificationOnPebble(title: String, notificationText: String) { + TODO("Not yet implemented") + } + + @JavascriptInterface + override fun getAccountToken(): String { + TODO("Not yet implemented") + } + + @JavascriptInterface + override fun getWatchToken(): String { + TODO("Not yet implemented") + } + + @JavascriptInterface + override fun showToast(toast: String) { + TODO("Not yet implemented") + } + + @JavascriptInterface + override fun showNotificationOnPebble(jsonObjectStringNotificationData: String) { + TODO("Not yet implemented") + } + + @JavascriptInterface + override fun openURL(url: String): String { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt new file mode 100644 index 00000000..1e5db498 --- /dev/null +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt @@ -0,0 +1,40 @@ +package io.rebble.cobble.shared.js + +import android.net.Uri +import android.webkit.JavascriptInterface +import io.rebble.cobble.shared.Logging +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class WebViewPrivatePKJSInterface(private val jsRunner: WebViewJsRunner, private val scope: CoroutineScope): PrivatePKJSInterface { + + @JavascriptInterface + override fun privateLog(message: String) { + TODO("Not yet implemented") + } + + @JavascriptInterface + override fun logInterceptedSend() { + TODO("Not yet implemented") + } + + @JavascriptInterface + override fun logInterceptedRequest() { + TODO("Not yet implemented") + } + + @JavascriptInterface + override fun getVersionCode(): Int { + TODO("Not yet implemented") + } + + @JavascriptInterface + fun startupScriptHasLoaded(url: String) { + Logging.d("Startup script has loaded: $url") + val uri = Uri.parse(url) + val params = uri.getQueryParameter("params") + scope.launch { + jsRunner.loadAppJs(params) + } + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt new file mode 100644 index 00000000..b25a9ee9 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt @@ -0,0 +1,8 @@ +package io.rebble.cobble.shared.js + +import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo + +abstract class JsRunner(val appInfo: PbwAppInfo, val jsPath: String) { + abstract suspend fun start() + abstract suspend fun stop() +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSInterface.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSInterface.kt new file mode 100644 index 00000000..78d5c8c4 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSInterface.kt @@ -0,0 +1,27 @@ +package io.rebble.cobble.shared.js + +interface PKJSInterface { + fun showSimpleNotificationOnPebble(title: String, notificationText: String) + + /** + * Get account token + * Sideloaded apps: hash of token and app UUID + * Appstore apps: hash of token and developer ID + * //TODO: offline token + */ + fun getAccountToken(): String + + /** + * Get token of the watch for storing settings + * Sideloaded apps: hash of watch serial and app UUID + * Appstore apps: hash of watch serial and developer ID + */ + fun getWatchToken(): String + fun showToast(toast: String) + fun showNotificationOnPebble(jsonObjectStringNotificationData: String) + + /** + * Open a URL e.g. configuration page + */ + fun openURL(url: String): String +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PrivatePKJSInterface.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PrivatePKJSInterface.kt new file mode 100644 index 00000000..06f45474 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PrivatePKJSInterface.kt @@ -0,0 +1,8 @@ +package io.rebble.cobble.shared.js + +interface PrivatePKJSInterface { + fun privateLog(message: String) + fun logInterceptedSend() + fun logInterceptedRequest() + fun getVersionCode(): Int +} \ No newline at end of file From 3e96add0d2c9258be8e1915111d0cefe7f5ef1ac Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 6 Aug 2024 23:10:39 +0100 Subject: [PATCH 02/20] start impl of js interfaces --- android/app/build.gradle | 0 android/app/build.gradle.kts | 2 +- .../cobble/shared/js/WebViewJsRunner.kt | 6 +++++- .../cobble/shared/js/WebViewPKJSInterface.kt | 21 ++++++++++++++----- .../shared/src/androidMain/proguard-rules.pro | 16 ++++++++++++++ .../io/rebble/cobble/shared/js/JsRunner.kt | 1 + 6 files changed, 39 insertions(+), 7 deletions(-) delete mode 100644 android/app/build.gradle create mode 100644 android/shared/src/androidMain/proguard-rules.pro diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index e69de29b..00000000 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a75a24e9..d55a0396 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -73,7 +73,7 @@ android { buildTypes { getByName("release") { isMinifyEnabled = true - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", rootProject.file("shared/androidMain/proguard-rules.pro")) signingConfig = signingConfigs.getByName("release") } getByName("debug") { diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt index 49d163a3..5a59bec3 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt @@ -24,7 +24,7 @@ class WebViewJsRunner(val context: Context, private val scope: CoroutineScope, a private var webView: WebView? = null private val initializedLock = Object() - private val publicJsInterface = WebViewPKJSInterface() + private val publicJsInterface = WebViewPKJSInterface(this) private val privateJsInterface = WebViewPrivatePKJSInterface(this, scope) private val interfaces = setOf( Pair(API_NAMESPACE, publicJsInterface), @@ -196,6 +196,10 @@ class WebViewJsRunner(val context: Context, private val scope: CoroutineScope, a } } + override fun loadUrl(url: String) { + TODO() + } + private suspend fun loadApp(url: String) { check(webView != null) { "WebView not initialized" } withContext(Dispatchers.Main) { diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt index 46e809ec..a5864934 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt @@ -1,8 +1,15 @@ package io.rebble.cobble.shared.js +import android.content.Context import android.webkit.JavascriptInterface +import android.widget.Toast +import io.rebble.cobble.shared.Logging +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class WebViewPKJSInterface(private val jsRunner: JsRunner): PKJSInterface, KoinComponent { + val context: Context by inject() -class WebViewPKJSInterface: PKJSInterface { @JavascriptInterface override fun showSimpleNotificationOnPebble(title: String, notificationText: String) { TODO("Not yet implemented") @@ -10,17 +17,19 @@ class WebViewPKJSInterface: PKJSInterface { @JavascriptInterface override fun getAccountToken(): String { - TODO("Not yet implemented") + //TODO + return "" } @JavascriptInterface override fun getWatchToken(): String { - TODO("Not yet implemented") + //TODO + return "" } @JavascriptInterface override fun showToast(toast: String) { - TODO("Not yet implemented") + Toast.makeText(context, toast, Toast.LENGTH_SHORT).show() } @JavascriptInterface @@ -30,6 +39,8 @@ class WebViewPKJSInterface: PKJSInterface { @JavascriptInterface override fun openURL(url: String): String { - TODO("Not yet implemented") + Logging.d("Opening URL") + jsRunner.loadUrl(url) + return url } } \ No newline at end of file diff --git a/android/shared/src/androidMain/proguard-rules.pro b/android/shared/src/androidMain/proguard-rules.pro new file mode 100644 index 00000000..9bbfc6d1 --- /dev/null +++ b/android/shared/src/androidMain/proguard-rules.pro @@ -0,0 +1,16 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +-keepclassmembers class io.rebble.cobble.shared.js.WebViewPKJSInterface { + public *; +} +-keepclassmembers class io.rebble.cobble.shared.js.WebViewPrivatePKJSInterface { + public *; +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt index b25a9ee9..9b6cad9d 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt @@ -5,4 +5,5 @@ import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo abstract class JsRunner(val appInfo: PbwAppInfo, val jsPath: String) { abstract suspend fun start() abstract suspend fun stop() + abstract fun loadUrl(url: String) } \ No newline at end of file From 846b8b65e50f682bfe67886ae6e94bb3d8102b75 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 28 Aug 2024 12:25:16 -0400 Subject: [PATCH 03/20] create platform-independent factory for js runner --- .../cobble/shared/js/JsRunnerFactory.android.kt | 16 ++++++++++++++++ .../rebble/cobble/shared/js/JsRunnerFactory.kt | 9 +++++++++ .../cobble/shared/js/JsRunnerFactory.ios.kt | 9 +++++++++ 3 files changed, 34 insertions(+) create mode 100644 android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt create mode 100644 android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt new file mode 100644 index 00000000..c93af576 --- /dev/null +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt @@ -0,0 +1,16 @@ +package io.rebble.cobble.shared.js + +import android.content.Context +import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo +import kotlinx.coroutines.CoroutineScope +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +actual object JsRunnerFactory: KoinComponent { + private val context: Context by inject() + actual fun createJsRunner( + scope: CoroutineScope, + appInfo: PbwAppInfo, + jsPath: String + ): JsRunner = WebViewJsRunner(context, scope, appInfo, jsPath) +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt new file mode 100644 index 00000000..ed98109c --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt @@ -0,0 +1,9 @@ +package io.rebble.cobble.shared.js + +import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo +import kotlinx.coroutines.CoroutineScope +import org.koin.core.component.KoinComponent + +expect object JsRunnerFactory: KoinComponent { + fun createJsRunner(scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner +} \ No newline at end of file diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt new file mode 100644 index 00000000..242f52d5 --- /dev/null +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt @@ -0,0 +1,9 @@ +package io.rebble.cobble.shared.js + +import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo +import kotlinx.coroutines.CoroutineScope +import org.koin.core.component.KoinComponent + +actual object JsRunnerFactory: KoinComponent { + actual fun createJsRunner(scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner = TODO() +} \ No newline at end of file From 89a09e6f84972baf1d2abd00ca684d7380c7ca58 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 28 Aug 2024 22:43:31 -0400 Subject: [PATCH 04/20] implement enough to run basic hello world, make js print test --- android/app/build.gradle.kts | 1 + .../app/src/androidTest/assets/print_test.js | 4 ++ .../shared/js/JsRunnerFactory.android.kt | 4 +- .../cobble/shared/js/WebViewJsRunner.kt | 37 +++++++++++++++---- .../shared/js/WebViewPrivatePKJSInterface.kt | 27 ++++++++++++++ .../cobble/shared/js/JsRunnerFactory.kt | 3 +- .../cobble/shared/js/JsRunnerFactory.ios.kt | 3 +- 7 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 android/app/src/androidTest/assets/print_test.js diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index d55a0396..ad647e4b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -37,6 +37,7 @@ android { java.srcDirs("src/main/kotlin") } getByName("androidTest") { + assets.srcDirs("src/androidTest/assets") java.srcDirs("src/androidTest/kotlin") } } diff --git a/android/app/src/androidTest/assets/print_test.js b/android/app/src/androidTest/assets/print_test.js new file mode 100644 index 00000000..afc18e1d --- /dev/null +++ b/android/app/src/androidTest/assets/print_test.js @@ -0,0 +1,4 @@ +Pebble.addEventListener('ready', function() { + // PebbleKit JS is ready! + console.log('PebbleKit JS ready!'); +}); \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt index c93af576..8db12a60 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt @@ -3,6 +3,7 @@ package io.rebble.cobble.shared.js import android.content.Context import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -10,7 +11,8 @@ actual object JsRunnerFactory: KoinComponent { private val context: Context by inject() actual fun createJsRunner( scope: CoroutineScope, + connectedAddress: StateFlow, appInfo: PbwAppInfo, jsPath: String - ): JsRunner = WebViewJsRunner(context, scope, appInfo, jsPath) + ): JsRunner = WebViewJsRunner(context, connectedAddress, scope, appInfo, jsPath) } \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt index 5a59bec3..0a390be5 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt @@ -12,9 +12,13 @@ import io.rebble.cobble.shared.Logging import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json -class WebViewJsRunner(val context: Context, private val scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner(appInfo, jsPath) { +class WebViewJsRunner(val context: Context, private val connectedAddress: StateFlow, private val scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner(appInfo, jsPath) { companion object { const val API_NAMESPACE = "Pebble" @@ -213,12 +217,31 @@ class WebViewJsRunner(val context: Context, private val scope: CoroutineScope, a } suspend fun loadAppJs(params: String?) { - check(webView != null) { "WebView not initialized" } - if (params == null) { - Logging.e("No params passed to loadAppJs") - return - } + webView?.let { webView -> + if (params == null) { + Logging.e("No params passed to loadAppJs") + return + } + + val paramsDecoded = Uri.decode(params) + val paramsJson = Json.decodeFromString>(paramsDecoded) + val jsUrl = paramsJson["loadUrl"] + if (jsUrl.isNullOrBlank() || !jsUrl.endsWith(".js")) { + Logging.e("loadUrl passed to loadAppJs empty or invalid") + return + } - + withContext(Dispatchers.Main) { + webView.loadUrl("javascript:loadScript('$jsUrl')") + } + } ?: error("WebView not initialized") + } + + suspend fun signalReady() { + val readyDeviceIds = listOf(connectedAddress.value ?: return) + val readyJson = Json.encodeToString(readyDeviceIds) + withContext(Dispatchers.Main) { + webView?.loadUrl("javascript:signalReady(${Uri.encode(readyJson)})") + } } } \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt index 1e5db498..80d317e6 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt @@ -37,4 +37,31 @@ class WebViewPrivatePKJSInterface(private val jsRunner: WebViewJsRunner, private jsRunner.loadAppJs(params) } } + + @JavascriptInterface + fun privateFnLocalStorageWrite(key: String, value: String) { + TODO("Not yet implemented") + } + + @JavascriptInterface + fun privateFnLocalStorageRead(key: String): String { + TODO("Not yet implemented") + } + + @JavascriptInterface + fun privateFnLocalStorageReadAll(): String { + return "{}" + } + + @JavascriptInterface + fun privateFnLocalStorageReadAll_AtPreregistrationStage(baseUriReference: String): String { + return privateFnLocalStorageReadAll() + } + + @JavascriptInterface + fun signalAppScriptLoadedByBootstrap() { + scope.launch { + jsRunner.signalReady() + } + } } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt index ed98109c..2f187164 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt @@ -2,8 +2,9 @@ package io.rebble.cobble.shared.js import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent expect object JsRunnerFactory: KoinComponent { - fun createJsRunner(scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner + fun createJsRunner(scope: CoroutineScope, connectedAddress: StateFlow, appInfo: PbwAppInfo, jsPath: String): JsRunner } \ No newline at end of file diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt index 242f52d5..238c3f76 100644 --- a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt @@ -2,8 +2,9 @@ package io.rebble.cobble.shared.js import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent actual object JsRunnerFactory: KoinComponent { - actual fun createJsRunner(scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner = TODO() + actual fun createJsRunner(scope: CoroutineScope, connectedAddress: StateFlow, appInfo: PbwAppInfo, jsPath: String): JsRunner = TODO() } \ No newline at end of file From b90187274f0c9357f9797a9c1280cd7fbb485d85 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 28 Aug 2024 22:43:45 -0400 Subject: [PATCH 05/20] js print test --- .../cobble/shared/js/WebViewJsRunnerTest.kt | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt b/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt index b21ac075..3043b331 100644 --- a/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt +++ b/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt @@ -4,19 +4,41 @@ import android.content.Context import androidx.test.platform.app.InstrumentationRegistry import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import io.rebble.libpebblecommon.util.runBlocking +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.json.Json import org.junit.Before import org.junit.Test +import java.io.File class WebViewJsRunnerTest { private lateinit var context: Context private val json = Json {ignoreUnknownKeys = true} + private val coroutineScope = CoroutineScope(Dispatchers.Default) @Before fun setUp() { context = InstrumentationRegistry.getInstrumentation().targetContext } + + // Copies from the android_assets of the test to the sdcard so the WebView can access it + private fun assetsToSdcard(file: String): String { + val sdcardPath = context.getExternalFilesDir(null)!!.absolutePath + val testPath = "$sdcardPath/test" + File(testPath).mkdir() + val testFile = "$testPath/$file" + val assetManager = InstrumentationRegistry.getInstrumentation().getContext().getAssets() + val inputStream = assetManager.open(file) + val outputStream = File(testFile).outputStream() + inputStream.copyTo(outputStream) + inputStream.close() + outputStream.close() + return testFile + } + @Test fun test() = runBlocking { val appInfo: PbwAppInfo = json.decodeFromString( @@ -54,9 +76,10 @@ class WebViewJsRunnerTest { } """.trimIndent() ) - val printTestPath = "file:///android_asset/print_test.js" - val webViewJsRunner = WebViewJsRunner(context, appInfo, printTestPath) + val printTestPath = assetsToSdcard("print_test.js") + val webViewJsRunner = WebViewJsRunner(context, MutableStateFlow("dummy"), coroutineScope, appInfo, printTestPath) webViewJsRunner.start() - delay(5000) + delay(1000) + webViewJsRunner.stop() } } \ No newline at end of file From 0d096feeef24f35a2f46e80b910af7068dc96d5e Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 28 Aug 2024 22:45:21 -0400 Subject: [PATCH 06/20] pkjs init script --- .../src/androidMain/assets/webview_startup.js | 734 +++++++++++++++++- 1 file changed, 733 insertions(+), 1 deletion(-) diff --git a/android/shared/src/androidMain/assets/webview_startup.js b/android/shared/src/androidMain/assets/webview_startup.js index 027adb8c..aa9cfc65 100644 --- a/android/shared/src/androidMain/assets/webview_startup.js +++ b/android/shared/src/androidMain/assets/webview_startup.js @@ -1 +1,733 @@ -/** @namespace Pebble */ \ No newline at end of file +/** @namespace Pebble */ +// important note for this script: it expects that the native Pebble object has been +// added to the webview (via addJavascriptInterface) *before* it runs +// +// as of Android 4.4, this should be 'automatic' since the WebView source code serially executes all calls +// into it on its own thread. If this ever changes, it is worth revisiting this. + +/**** private calls ***/ +_appMessageAckCallbacks = {}; +_appMessageNackCallbacks = {}; + +// Override XMLHttpRequest + +XMLHttpRequest.prototype.uniqueID = function( ) { + if (!this.uniqueIDMemo) { + this.uniqueIDMemo = Math.floor(Math.random( ) * 1000); + } + return this.uniqueIDMemo; +} + +var sDefaultTimeout = 30000; + +XMLHttpRequest.prototype.oldSend = XMLHttpRequest.prototype.send; + +var newSend = function(a) { + var xhr = this; + _Pebble.privateLog("[" + xhr.uniqueID( ) + "] intercepted send (" + a + ") timeout = " + xhr.timeout + + " hasOnTimeout = " + (undefined != xhr.ontimeout) + " async = " + xhr.pebbleAsync); + _Pebble.logInterceptedSend(); + if (0 == xhr.timeout) { + if (!xhr.pebbleAsync) { + _Pebble.privateLog("[" + xhr.uniqueID( ) + "] intercepted; not setting timeout for synchronous request"); + } else { + _Pebble.privateLog("[" + xhr.uniqueID( ) + "] intercepted; setting missing timeout to " + sDefaultTimeout); + xhr.timeout = sDefaultTimeout; + } + } + + var onload = function( ) { + _Pebble.privateLog("[" + xhr.uniqueID( ) + "] intercepted load: " + xhr.status); + }; + var onerror = function( ) { + _Pebble.privateLog("[" + xhr.uniqueID( ) + "] intercepted error: " + xhr.status); + }; + var ontimeout = function( ) { + _Pebble.privateLog("[" + xhr.uniqueID( ) + "] intercepted timeout: " + xhr.status); + }; + + xhr.addEventListener("load", onload, false); + xhr.addEventListener("error", onerror, false); + xhr.addEventListener("timeout", ontimeout, false); + + xhr.oldSend(a); +} + +XMLHttpRequest.prototype.send = newSend; + +XMLHttpRequest.prototype.oldOpen = XMLHttpRequest.prototype.open; + +var newOpen = function(method, url, async, user, password) { + _Pebble.privateLog("[" + this.uniqueID( ) + "] intercepted open (" + method + " , " + url + " , async = " + async + ")"); + // When async parameter value is omitted, use true as default + if (arguments.length < 3 || undefined == async) { + async = true; + } + // Store the async parameter in our own member + this.pebbleAsync = async; + + // Pass the parameters through to super() according to how thehy were presented to us + if (arguments.length > 4) { + this.oldOpen(method, url, async, user, password); + } else if (arguments.length > 3) { + this.oldOpen(method, url, async, user); + } else { + this.oldOpen(method, url, async); + } +} + +XMLHttpRequest.prototype.open = newOpen; + +// End overriding XMLHttpRequest + +// Override location request +navigator.geolocation.__proto__.originalGetCurrentPosition = navigator.geolocation.getCurrentPosition; +var overridenGetCurrentPosition = function(locationSuccess, locationError, locationOptions) { + _Pebble.logLocationRequest(); + navigator.geolocation.originalGetCurrentPosition(locationSuccess, locationError, locationOptions); +} +navigator.geolocation.__proto__.getCurrentPosition = overridenGetCurrentPosition; +// End override location request + +function isFunction(functionToCheck) { + if ((functionToCheck == null) || (functionToCheck == undefined)) { + return false; + } + var getType = {}; + return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; +} + +var PebbleEventListener = { + events: [], + + addEventListener: function( type, callback, useCapture ) { + + if( !this.events[type] ) { + this.events[type] = []; + + // call the event initializer, if this is the first time we + // bind to this event. + if( typeof(this._eventInitializers[type]) == 'function' ) { + this._eventInitializers[type](); + } + } + + if (isFunction(callback)) { + this.events[type].push( callback ); + } + }, + + removeEventListener: function( type, callback ) { + var listeners = this.events[ type ]; + if( !listeners ) { return; } + + for( var i = listeners.length; i--; ) { + if( listeners[i] === callback ) { + listeners.splice(i, 1); + } + } + }, + + _eventInitializers: {}, + dispatchEvent: function( event ) { + + var listeners = this.events[ event.type ]; + if( !listeners ) { return false; } + + // fire off a duplicate listener array, in case any of them add new events + listeners = listeners.slice(0); + var removeList = []; + var returnVal = true; + for( var i = 0; i < listeners.length; i++ ) { + try { + var removeListener = listeners[i]( event ); + if (removeListener === true) { + removeList.push(i); + } + } + catch (e) { + //guard against bad external code calls + console.log('jskit_system :: PebbleEventListener : bad dispatch on event '+event.type + ': ' + e); + returnVal = false; + } + } + for (var i = (removeList.length)-1; i >= 0;i--) { + try { + listeners.splice(removeList[i],1); + console.log('jskit_system :: PebbleEventListener : post-dispatch removed listener ' + removeList[i] + + ' on event '+ event.type); + } + catch (e) { + console.log('jskit_system :: PebbleEventListener : post-dispatch failed to remove listener ' + + removeList[i] + + ' on event '+ event.type); + } + } + return returnVal; + } +} + +function signalWebviewOpenedEvent(data) { + var event = document.createEvent('Event'); + event.initEvent('webviewopened',true,true); + event.type = 'webviewopened'; + event.data = data; + event.opened = data; + PebbleEventListener.dispatchEvent(event); +} + +function signalReady(data) { + // _initLocalStorageMonitor(); + var event = document.createEvent('Event'); + event.initEvent('ready',true,true); + event.type = 'ready'; + event.data = data; + event.ready = data; + var success = PebbleEventListener.dispatchEvent(event); + //callback into Pebble. (jskit) to + // confirm this app is now armed n' ready in all respects + // is able to execute JS code. + // this doesn't guarantee that the 3rd party JS will continue to run + // as it may be itself broken/have errors. + // however, getting here means that the bootstrap and loading has succeeded + + try { + _Pebble.privateFnConfirmReadySignal(success); + //start a heartbeat timer + setInterval(function(){ + try { + _Pebble.privateFnHeartbeatPeriodic(); + } + catch(exc) {} + },5000); + } + catch (ex) { + } +} + +function signalWebviewClosedEvent(data) { + var event = document.createEvent('Event'); + event.initEvent('webviewclosed',true,true); + event.type = 'webviewclosed'; + try { + var decodedData; + + if (data && data.length > 0) { + decodedData = decodeURIComponent(data); + } + event.data = decodedData; + event.response = decodedData; + } + catch (e) { + event.data = data; + event.response = data; + } + PebbleEventListener.dispatchEvent(event); +} + +function signalNewAppMessageData(data) { + var event = document.createEvent('Event'); + event.initEvent('appmessage',true,true); + event.type = 'appmessage'; + try { + event.payload = JSON.parse(data); + } + catch (e) { + console.log('failed to JSON.parse data passed in'); + event.payload = {}; + } + event.data = event.payload; + PebbleEventListener.dispatchEvent(event); +} + +function signalAppMessageAck(data) { + var event = document.createEvent('Event'); + event.initEvent('appmessage_ack',true,true); + event.type = 'appmessage_ack'; + try { + event.payload = JSON.parse(data); + } + catch (e) { + console.log('failed to JSON.parse data passed in'); + event.payload = {}; + } + + event.data = event.payload; + PebbleEventListener.dispatchEvent(event); + + if (event.payload.transactionId != undefined) { + removeAppMessageCallbacksForTransactionId(event.payload.transactionId); + } +} + +function signalAppMessageNack(data) { + var event = document.createEvent('Event'); + event.initEvent('appmessage_nack',true,true); + event.type = 'appmessage_nack'; + try { + event.payload = JSON.parse(data); + } + catch (e) { + console.log('failed to JSON.parse data passed in'); + event.payload = {}; + } + + event.data = event.payload; + PebbleEventListener.dispatchEvent(event); + + if (event.payload.transactionId != undefined) { + removeAppMessageCallbacksForTransactionId(event.payload.transactionId); + } +} + +function removeAppMessageCallbacksForTransactionId(tid) { + if (_appMessageAckCallbacks[tid]) { + PebbleEventListener.removeEventListener('appmessage_ack',_appMessageAckCallbacks[tid]); + } + + if (_appMessageNackCallbacks[tid]) { + PebbleEventListener.removeEventListener('appmessage_nack',_appMessageNackCallbacks[tid]); + } + + _appMessageAckCallbacks[tid] = undefined; + _appMessageNackCallbacks[tid] = undefined; +} + +function signalSettingsWebuiLaunchOpportunity(data) { + + var event = document.createEvent('Event'); + event.initEvent('showConfiguration',true,true); + event.type = 'showConfiguration'; + try { + event.payload = JSON.parse(data); + } + catch (e) { + console.log('failed to JSON.parse data passed in'); + event.payload = {}; + } + event.data = event.payload; + PebbleEventListener.dispatchEvent(event); + + //it was called something else before so i am maintaing that + //don't ask me to remove it, at least for the next 3-4 weeks + + var earlyVersionCompatEvent = document.createEvent('Event'); + earlyVersionCompatEvent.initEvent('settings_webui_allowed',true,true); + earlyVersionCompatEvent.type = 'settings_webui_allowed'; + try { + earlyVersionCompatEvent.payload = JSON.parse(data); + } + catch (e) { + console.log('failed to JSON.parse data passed in'); + earlyVersionCompatEvent.payload = {}; + } + earlyVersionCompatEvent.data = earlyVersionCompatEvent.payload; + PebbleEventListener.dispatchEvent(earlyVersionCompatEvent); +} + +function _initLocalStorageMonitor(usedPriorToScriptLoadComplete,loadingUrl) { + localStorage.clear(); + var storageItemsUnparsed = {}; + if (usedPriorToScriptLoadComplete == true) { + storageItemsUnparsed = + _Pebble.privateFnLocalStorageReadAll_AtPreregistrationStage(loadingUrl); + } + else { + storageItemsUnparsed = + _Pebble.privateFnLocalStorageReadAll(); + } + + var storageItems; + if (storageItemsUnparsed) { + storageItems = JSON.parse(storageItemsUnparsed); + } + + var storageKeys = Object.keys(storageItems); + storageKeys.forEach(function(key, idx, keys) { + localStorage[key] = storageItems[key]; + }); + + var iframe = document.createElement("iframe"); + document.documentElement.appendChild(iframe); + + iframe.contentWindow.addEventListener("storage", function(event) { + if (!event.key) { + return; + } + + if (event.storageArea != iframe.contentWindow.localStorage) { + console.log("storage event not fired on localstorage. ignoring."); + return; + } + + if (event.newValue === null) { + _Pebble.privateFnLocalStorageRemove(event.key); + } else { + _Pebble.privateFnLocalStorageWrite(event.key, event.newValue); + } + }, false); +} + +function signalLoaded() { + console.log("signalLoaded"); + try { + console.log("inside try-bridge-active"); + } + catch (e) { + console.log("signalLoaded : bridge not yet ready...retry-delay"); + setTimeout(function(){signalLoaded()},100); + return; + } + + _Pebble.signalAppScriptLoadedByBootstrap(); + console.log("signalLoaded (finalized)"); +} + +function signalBodyLoaded() { + console.log("signalBodyLoaded"); +} + +function loadScript(url) +{ + console.log("loadScript "+url); + // adding the script tag to the head + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + script.charset = "UTF-8" + + // then bind the event to the callback function + // there are several events for cross browser compatibility + script.onreadystatechange = signalLoaded; + script.onload = signalLoaded; + + // LOCALSTORE-BEFORE-READY-EVENT HXMOD + _initLocalStorageMonitor(true,url); + + // fire the loading + head.appendChild(script); +} + +function loadBody(url) +{ + console.log("loadBody "+url); + var body = document.getElementsByTagName('body')[0]; + var newbody = document.createElement('body'); + + newbody.onreadystatechange = signalBodyLoaded; + newbody.onload = signalBodyLoaded; + newbody.src = url; + //fire the loading + console.log("document replacechild"); +} + +function httpGetNativeJsSynchro(theUrl) +{ + var xmlHttp = null; + + xmlHttp = new XMLHttpRequest(); + xmlHttp.open( "GET", theUrl, false ); + xmlHttp.send( null ); + return xmlHttp.responseText; +} + +function scriptHasEmbeddedAppInfo() { + return (appinfo && (typeof appinfo.info != 'undefined')); +} + +function pingWebcontext(opt) { + if (opt != undefined) { + _Pebble.pong(opt); + } + else { + _Pebble.pong("(no-opt)"); + } +} + +var getTimelineSubscribeToTopicURL = function(topic) { + // TODO: Get and use from future boot.json URL template + var encodedTopic = encodeURIComponent(topic); + return "https://timeline-api.getpebble.com/v1/user/subscriptions/" + encodedTopic; +}; + +var getTimelineSubscriptionsListURL = function() { + // TODO: Get and use from future boot.json URL template + return "https://timeline-api.getpebble.com/v1/user/subscriptions"; +}; + +/**** public calls ***/ + +/** + * Adds a listener for Pebble JS events. + * + * Valid event types: + * + * appmessage: watch sent an AppMessage to JS. AppMessage is contained in the payload property of the event object. + * it consists of key-value pairs. the keys are strings containing integers, or aliases for keys defined in appinfo.json. + * values will be numbers, or arrays of characters + * + * showConfiguration: the user has requested that a configuration view be loaded. this could be caused by an + * initial app install, or by the user tapping the configuration button in the Pebble phone app + * + * webviewclosed: an open webview was closed by the user. if the webview had a response, it will be contained in + * the response property + * + * @function addEventListener + * @memberof Pebble + * @param type event type + * @param callback function to receive event + * @param useCapture true if events should be captured + */ +Pebble.addEventListener = function(type, callback, useCapture) { + PebbleEventListener.addEventListener(type, callback, useCapture); +}; + +/** + * remove an existing event listener + * @function removeEventListener + * @memberof Pebble + * @param type type of event + * @param callback existing registered callback + */ +Pebble.removeEventListener = function(type, callback) { + PebbleEventListener.removeEventListener(type, callback); +}; + +/** + * send an AppMessage to app running on Pebble + * @function sendAppMessage + * @memberof Pebble + * @param jsonAppMessage an object containing key-value pairs to send to the watch. keys must be strings containing + * integers, or aliases defined in appinfo.json. values must be integers, arrays of bytes, or strings. values + * in arrays greater than 255 will be mod 255 before sending. + * @param callbackForAck callback to run when watch sends ack on appmessage + * @param callbackForNack callback to run when watch sends nack on appmessage, or sending failed + * @returns transaction id + */ +Pebble.sendAppMessage = function(rawJsonObjectToSend,callbackForAck,callbackForNack) { + var transactionId = null; + try { + transactionId = _Pebble.sendAppMessageString(JSON.stringify(rawJsonObjectToSend)); + } + catch (e) { + console.log('misuse of sendAppMessage(raw JSON object to send)...check that parameter'); + } + + if (transactionId == null) { + throw "Error sending app message. Unknown key."; + } + + + if (callbackForAck != undefined) { + var wrappedCallbackforAck = function(e) { + try { + if (e.data.transactionId == transactionId) { + // console.log("calling Ack callback for transactionID: " + transactionId); + callbackForAck(e); + } else { + // console.log("ack fakeout"); + } + } + catch (exx) {} + + } + _appMessageAckCallbacks[transactionId] = wrappedCallbackforAck; + + try { + PebbleEventListener.addEventListener('appmessage_ack',wrappedCallbackforAck); + } + catch (e) { + console.log('misuse of sendAppMessage(raw JSON object to send): ack callback param is bad'); + } + } + + if (callbackForNack != undefined) { + var wrappedCallbackforNack = function(e) { + try { + if (e.data.transactionId == transactionId) { + // console.log("calling Nack callback"); + callbackForNack(e); + } else { + // console.log("Nack fakeout"); + } + } + catch (exx) {} + } + _appMessageNackCallbacks[transactionId] = wrappedCallbackforNack; + try { + PebbleEventListener.addEventListener('appmessage_nack',wrappedCallbackforNack); + } + catch (e) { + console.log('misuse of sendAppMessage(raw JSON object to send): nack callback param is bad'); + } + } + + return transactionId; +} + +function signalTimelineTokenSuccess(data) { + var event = document.createEvent('Event'); + + event.initEvent('getTimelineTokenSuccess',true,true); + event.type = 'getTimelineTokenSuccess'; + try { + event.payload = JSON.parse(data); + } + catch (e) { + console.log('failed to JSON.parse data passed in'); + event.payload = {}; + } + event.data = event.payload; + PebbleEventListener.dispatchEvent(event); +} + +function signalTimelineTokenFailure(data) { + var event = document.createEvent('Event'); + + event.initEvent('getTimelineTokenFailure',true,true); + event.type = 'getTimelineTokenFailure'; + try { + event.payload = JSON.parse(data); + } + catch (e) { + console.log('failed to JSON.parse data passed in'); + event.payload = {}; + } + event.data = event.payload; + PebbleEventListener.dispatchEvent(event); +} + +Pebble.getTimelineToken = function(successCallback, failureCallback) { + try { + instanceCallId = _Pebble.getTimelineTokenAsync(); + + var successWrapper = function(e) { + callId = e.data.callId; + token = e.data.userToken; + if (callId == instanceCallId) { + successCallback(token); + PebbleEventListener.removeEventListener('getTimelineTokenSuccess', successWrapper); + PebbleEventListener.removeEventListener('getTimelineTokenFailure', failureWrapper); + } + }; + + var failureWrapper = function(e) { + callId = e.data.callId; + if (callId == instanceCallId) { + failureCallback(); + PebbleEventListener.removeEventListener('getTimelineTokenSuccess', successWrapper); + PebbleEventListener.removeEventListener('getTimelineTokenFailure', failureWrapper); + } + }; + + PebbleEventListener.addEventListener('getTimelineTokenSuccess', successWrapper); + PebbleEventListener.addEventListener('getTimelineTokenFailure', failureWrapper); + } catch (e) { + console.log('Error in the getTimelineToken method'); + } +} + +Pebble.timelineSubscribe = function(topic, successCb, errorCb) { + window.Pebble.getTimelineToken( + function(token) { + var url = getTimelineSubscribeToTopicURL(topic); + var xhr = new XMLHttpRequest(); + xhr.open("POST", url, true); + xhr.onload = function (e) { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + successCb(); + } else { + errorCb(); + } + } + }; + xhr.onerror = function (e) { + errorCb(); + }; + xhr.timeout = 15000; + xhr.setRequestHeader ("X-User-Token", token); + xhr.send(null); + }, + errorCb + ); +}; + +Pebble.timelineUnsubscribe = function(topic, successCb, errorCb) { + window.Pebble.getTimelineToken( + function(token) { + var url = getTimelineSubscribeToTopicURL(topic); + var xhr = new XMLHttpRequest(); + xhr.open("DELETE", url, true); + xhr.onload = function (e) { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + successCb(); + } else { + errorCb(); + } + } + }; + xhr.onerror = function (e) { + errorCb(); + }; + xhr.timeout = 15000; + xhr.setRequestHeader ("X-User-Token", token); + xhr.send(null); + }, + errorCb + ); +}; + +Pebble.timelineSubscriptions = function(successCb, errorCb) { + window.Pebble.getTimelineToken( + function(token) { + var url = getTimelineSubscriptionsListURL(); + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.onload = function (e) { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + try { + var responseJSON = JSON.parse(xhr.responseText); + successCb(responseJSON["topics"]); + } catch (err) { + errorCb("Malformed response from server: " + err); + } + } else { + errorCb("Error loading from server"); + } + } + }; + xhr.onerror = function (e) { + errorCb(e); + }; + xhr.timeout = 15000; + xhr.setRequestHeader ("X-User-Token", token); + xhr.send(null); + }, + function () { + errorCb("Error getting token"); + } + ); +}; + +Pebble.getActiveWatchInfo = function() { + var data = _Pebble.getActivePebbleWatchInfo(); + if (data === "") { + return null; + } else { + return JSON.parse(data); + } +} + +Pebble.appGlanceReload = function(appGlanceSlices, appGlanceReloadSuccessCallback, appGlanceReloadFailureCallback) { + var success = _Pebble.reloadAppGlances(JSON.stringify(appGlanceSlices)); + var callbackPayload = {"success": success}; + if (success === true) { + appGlanceReloadSuccessCallback(appGlanceSlices, callbackPayload); + } else { + appGlanceReloadFailureCallback(appGlanceSlices, callbackPayload); + } +} From 55f588474d243fb91477ead9791964ba78643e79 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 20 Sep 2024 19:50:59 +0100 Subject: [PATCH 07/20] don't double log device logs --- .../app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt index 905fd364..fc9f4b82 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt @@ -91,9 +91,6 @@ private suspend fun getDeviceLogs(deviceLogController: DeviceLogController): Lis return null } return flow.filterIsInstance() - .onEach { - Timber.d("Log line: ${it.timestamp.get()} ${it.filename.get()}:${it.line.get()} ${it.level.get()} ${it.messageText.get()}") - } .map { "${it.timestamp.get()} ${it.filename.get()}:${it.line.get()} ${it.level.get()} ${it.messageText.get()}" }.toList() From 0c7bf7f98b9a2b0866fdd68243dbcc9ec35101fa Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 22 Sep 2024 20:48:33 +0100 Subject: [PATCH 08/20] move connection scope, connection dependents into shared+add to PebbleDevice --- android/app/build.gradle.kts | 2 +- android/app/src/main/AndroidManifest.xml | 2 +- .../io/rebble/cobble/FlutterMainActivity.kt | 4 +- .../cobble/bluetooth/ConnectionLooper.kt | 43 +-- .../cobble/bluetooth/DeviceTransport.kt | 3 + .../cobble/bluetooth/scan/ClassicScanner.kt | 2 +- .../bridges/common/ConnectionFlutterBridge.kt | 12 +- .../common/PermissionCheckFlutterBridge.kt | 8 +- .../bridges/ui/ConnectionUiFlutterBridge.kt | 6 +- .../ui/FirmwareUpdateControlFlutterBridge.kt | 7 +- .../ui/PermissionControlFlutterBridge.kt | 4 +- .../cobble/datasources/PermissionChangeBus.kt | 31 --- .../cobble/datasources/WatchMetadataStore.kt | 15 -- .../io/rebble/cobble/di/AppComponent.kt | 2 - .../io/rebble/cobble/di/ServiceModule.kt | 28 +- .../cobble/handlers/AppMessageHandler.kt | 250 ------------------ .../cobble/handlers/AppRunStateHandler.kt | 41 --- .../rebble/cobble/handlers/SystemHandler.kt | 163 ------------ .../io/rebble/cobble/log/LogSendingTask.kt | 3 +- .../cobble/providers/PebbleKitProvider.kt | 6 +- .../cobble/service/ServiceLifecycleControl.kt | 5 +- .../PebbleDictionaryConverterTest.kt | 2 + android/gradle/libs.versions.toml | 8 + .../java/io/rebble/cobble/bluetooth/BlueIO.kt | 4 +- .../cobble/bluetooth/EmulatedPebbleDevice.kt | 4 +- android/shared/build.gradle.kts | 7 +- .../com/getpebble/android/kit/Constants.java | 0 .../kotlin}/com/getpebble/android/kit/LICENSE | 0 .../com/getpebble/android/kit/PebbleKit.java | 147 ++++------ .../android/kit/util/PebbleDictionary.java | 0 .../android/kit/util/PebbleTuple.java | 0 .../android/kit/util/SportsState.java | 0 .../cobble/shared/AndroidPlatformContext.kt | 6 +- .../rebble/cobble/shared/di/AndroidModule.kt | 11 + .../shared/domain/PermissionChangeBus.kt | 24 ++ .../notifications/NotificationListener.kt | 9 +- .../handlers/AndroidPlatformAppMessageIPC.kt | 140 ++++++++++ .../shared}/handlers/CalendarHandler.kt | 90 +++---- .../shared/handlers/SystemHandler.android.kt | 46 ++++ .../music/ActiveMediaSessionProvider.kt | 10 +- .../shared}/handlers/music/MusicHandler.kt | 84 +++--- .../cobble/shared/jobs}/CalendarSyncWorker.kt | 12 +- .../shared/js/JsRunnerFactory.android.kt | 2 +- .../cobble/shared/js/WebViewJsRunner.kt | 4 +- .../shared/util/AppInstallUtils.android.kt | 24 +- .../io/rebble/cobble/shared}/util/Intent.kt | 2 +- .../shared/util}/PebbleDictionaryConverter.kt | 2 +- .../rebble/cobble/shared}/util/Permissions.kt | 2 +- .../io/rebble/cobble/shared/util/ZipUtils.kt | 2 + .../util/coroutines/BroadcastReceiver.kt | 2 +- .../rebble/cobble/shared/PlatformContext.kt | 3 + .../cobble/shared/di/LibPebbleModule.kt | 16 +- .../cobble/shared/di/PebbleDeviceModule.kt | 23 ++ .../io/rebble/cobble/shared/di/StateModule.kt | 3 - .../shared/domain/calendar/CalendarSync.kt | 21 +- .../shared/domain/common/PebbleDevice.kt | 24 +- .../shared/domain/state/ConnectionState.kt | 1 - .../shared/handlers/AppInstallHandler.kt | 16 +- .../shared/handlers/AppMessageHandler.kt | 167 ++++++++++++ .../shared/handlers/AppRunStateHandler.kt | 40 +++ .../shared/handlers/CalendarActionHandler.kt | 64 ++--- .../cobble/shared/handlers/SystemHandler.kt | 156 +++++++++++ .../cobble/shared/js/JsRunnerFactory.kt | 2 +- .../io/rebble/cobble/shared/js/PKJSApp.kt | 45 ++++ .../shared/middleware/PutBytesController.kt | 2 +- .../cobble/shared/ui/view/home/TestPage.kt | 2 +- .../cobble/shared/util/AppInstallUtils.kt | 11 + .../cobble/shared/IOSPlatformContext.kt | 5 +- .../shared/handlers/SystemHandler.ios.kt | 13 + .../cobble/shared/js/JsRunnerFactory.ios.kt | 2 +- .../cobble/shared/util/AppInstallUtils.ios.kt | 6 + 71 files changed, 1036 insertions(+), 867 deletions(-) delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/datasources/PermissionChangeBus.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/datasources/WatchMetadataStore.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/handlers/AppMessageHandler.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/handlers/AppRunStateHandler.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt rename android/{app/src/main/java => shared/src/androidMain/kotlin}/com/getpebble/android/kit/Constants.java (100%) rename android/{app/src/main/java => shared/src/androidMain/kotlin}/com/getpebble/android/kit/LICENSE (100%) rename android/{app/src/main/java => shared/src/androidMain/kotlin}/com/getpebble/android/kit/PebbleKit.java (84%) rename android/{app/src/main/java => shared/src/androidMain/kotlin}/com/getpebble/android/kit/util/PebbleDictionary.java (100%) rename android/{app/src/main/java => shared/src/androidMain/kotlin}/com/getpebble/android/kit/util/PebbleTuple.java (100%) rename android/{app/src/main/java => shared/src/androidMain/kotlin}/com/getpebble/android/kit/util/SportsState.java (100%) create mode 100644 android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain}/notifications/NotificationListener.kt (96%) create mode 100644 android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AndroidPlatformAppMessageIPC.kt rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/androidMain/kotlin/io/rebble/cobble/shared}/handlers/CalendarHandler.kt (61%) create mode 100644 android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.android.kt rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/androidMain/kotlin/io/rebble/cobble/shared}/handlers/music/ActiveMediaSessionProvider.kt (94%) rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/androidMain/kotlin/io/rebble/cobble/shared}/handlers/music/MusicHandler.kt (80%) rename android/{app/src/main/kotlin/io/rebble/cobble/background => shared/src/androidMain/kotlin/io/rebble/cobble/shared/jobs}/CalendarSyncWorker.kt (72%) rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/androidMain/kotlin/io/rebble/cobble/shared}/util/Intent.kt (82%) rename android/{app/src/main/kotlin/io/rebble/cobble/middleware => shared/src/androidMain/kotlin/io/rebble/cobble/shared/util}/PebbleDictionaryConverter.kt (99%) rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/androidMain/kotlin/io/rebble/cobble/shared}/util/Permissions.kt (97%) rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/androidMain/kotlin/io/rebble/cobble/shared}/util/coroutines/BroadcastReceiver.kt (96%) create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/PebbleDeviceModule.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt create mode 100644 android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.ios.kt diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ad647e4b..c99eb87b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -138,7 +138,7 @@ dependencies { implementation("androidx.lifecycle:lifecycle-service:$lifecycleVersion") implementation("com.jakewharton.timber:timber:$timberVersion") implementation("androidx.core:core-ktx:$androidxCoreVersion") - implementation("androidx.work:work-runtime-ktx:$workManagerVersion") + implementation(libs.androidx.work.runtime.ktx) implementation("com.squareup.okio:okio:$okioVersion") implementation(libs.androidx.room.runtime) implementation(libs.kotlinx.datetime) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 69d45537..6f2304dc 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -197,7 +197,7 @@ android:foregroundServiceType="connectedDevice" android:permission="android.permission.FOREGROUND_SERVICE" /> diff --git a/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt b/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt index 8cd3757e..863fef65 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt @@ -13,11 +13,11 @@ import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.REQUEST_CODE_NOTIFICATIONS_POST -import io.rebble.cobble.datasources.PermissionChangeBus +import io.rebble.cobble.shared.domain.PermissionChangeBus import io.rebble.cobble.service.CompanionDeviceService import io.rebble.cobble.service.InCallService import io.rebble.cobble.shared.database.closeDatabase -import io.rebble.cobble.util.hasNotificationPostingPermission +import io.rebble.cobble.shared.util.hasNotificationPostingPermission import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.plus import java.net.URI diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index 77b012a1..dde6a468 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -39,22 +39,6 @@ class ConnectionLooper @Inject constructor( private var lastConnectedWatch: String? = null private var delayJob: Job? = null - fun negotiationsComplete(watch: PebbleDevice) { - if (connectionState.value is ConnectionState.Negotiating) { - _connectionState.value = ConnectionState.Connected(watch) - } else { - Timber.w("negotiationsComplete state mismatch!") - } - } - - fun recoveryMode(watch: PebbleDevice) { - if (connectionState.value is ConnectionState.Connected || connectionState.value is ConnectionState.Negotiating) { - _connectionState.value = ConnectionState.RecoveryMode(watch) - } else { - Timber.w("recoveryMode state mismatch!") - } - } - fun signalWatchPresence(macAddress: String) { _watchPresenceState.value = macAddress if (lastConnectedWatch == macAddress) { @@ -110,7 +94,7 @@ class ConnectionLooper @Inject constructor( getBluetoothStatus(context).first { bluetoothOn -> bluetoothOn } } - + val connectionScope = CoroutineScope(SupervisorJob() + errorHandler + Dispatchers.IO) + CoroutineName("ConnectionScope-$macAddress") try { blueCommon.startSingleWatchConnection(macAddress).collect { if (it is SingleConnectionStatus.Connected && connectionState.value !is ConnectionState.Connected && connectionState.value !is ConnectionState.RecoveryMode) { @@ -123,12 +107,15 @@ class ConnectionLooper @Inject constructor( if (it is SingleConnectionStatus.Connected) { retryTime = HALF_OF_INITAL_RETRY_TIME retries = 0 + _connectionState.value.watchOrNull?.connectionScope?.value = connectionScope } } } catch (_: CancellationException) { // Do nothing. Cancellation is OK } catch (e: Exception) { Timber.e(e, "Watch connection error") + } finally { + connectionScope.cancel("Connection ended") } if (isActive) { @@ -184,28 +171,6 @@ class ConnectionLooper @Inject constructor( } currentConnection?.cancel() } - - /** - * Get [CoroutineScope] that is active while watch is connected and cancelled if watch - * disconnects. - */ - fun getWatchConnectedScope( - context: CoroutineContext = EmptyCoroutineContext - ): CoroutineScope { - val scope = CoroutineScope(SupervisorJob() + errorHandler + context) - - scope.launch(Dispatchers.Unconfined) { - connectionState.collect { - if (it !is ConnectionState.Connected && - it !is ConnectionState.Negotiating && - it !is ConnectionState.RecoveryMode) { - scope.cancel() - } - } - } - - return scope - } } private fun SingleConnectionStatus.toConnectionStatus(): ConnectionState { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index be4b06f1..7a02a866 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -17,7 +17,10 @@ import io.rebble.cobble.datasources.FlutterPreferences import io.rebble.cobble.datasources.IncomingPacketsListener import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.ProtocolHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import timber.log.Timber import javax.inject.Inject diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/ClassicScanner.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/ClassicScanner.kt index 3feff37c..19381f36 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/ClassicScanner.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/ClassicScanner.kt @@ -6,7 +6,7 @@ import android.content.Context import android.content.IntentFilter import androidx.annotation.RequiresPermission import io.rebble.cobble.bluetooth.ScannedPebbleDevice -import io.rebble.cobble.util.coroutines.asFlow +import io.rebble.cobble.shared.util.coroutines.asFlow import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt index b898cb85..4f6115f5 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt @@ -4,22 +4,20 @@ import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.data.toPigeon -import io.rebble.cobble.datasources.WatchMetadataStore import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.shared.domain.state.ConnectionState +import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.* import kotlinx.coroutines.flow.combine import javax.inject.Inject -@OptIn(ExperimentalCoroutinesApi::class) class ConnectionFlutterBridge @Inject constructor( bridgeLifecycleController: BridgeLifecycleController, private val connectionLooper: ConnectionLooper, private val coroutineScope: CoroutineScope, - private val protocolHandler: ProtocolHandler, - private val watchMetadataStore: WatchMetadataStore + private val protocolHandler: ProtocolHandler ) : FlutterBridge, Pigeons.ConnectionControl { private val connectionCallbacks = bridgeLifecycleController .createCallbacks(Pigeons::ConnectionCallbacks) @@ -53,10 +51,10 @@ class ConnectionFlutterBridge @Inject constructor( statusObservingJob = coroutineScope.launch(Dispatchers.Main) { combine( connectionLooper.connectionState, - watchMetadataStore.lastConnectedWatchMetadata, - watchMetadataStore.lastConnectedWatchModel - ) { connectionState, watchMetadata, model -> + ConnectionStateManager.connectedWatchMetadata, + ) { connectionState, watchMetadata -> val bluetoothDevice = connectionState.watchOrNull + val model = watchMetadata?.running?.hardwarePlatform?.get()?.toInt() Pigeons.WatchConnectionStatePigeon.Builder() .setIsConnected(connectionState is ConnectionState.Connected || connectionState is ConnectionState.RecoveryMode) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PermissionCheckFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PermissionCheckFlutterBridge.kt index fd53d903..9bcc5090 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PermissionCheckFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PermissionCheckFlutterBridge.kt @@ -8,10 +8,10 @@ import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.pigeons.BooleanWrapper import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.util.hasBatteryExclusionPermission -import io.rebble.cobble.util.hasCallsPermission -import io.rebble.cobble.util.hasContactsPermission -import io.rebble.cobble.util.hasNotificationAccessPermission +import io.rebble.cobble.shared.util.hasBatteryExclusionPermission +import io.rebble.cobble.shared.util.hasCallsPermission +import io.rebble.cobble.shared.util.hasContactsPermission +import io.rebble.cobble.shared.util.hasNotificationAccessPermission import javax.inject.Inject class PermissionCheckFlutterBridge @Inject constructor( diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/ConnectionUiFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/ConnectionUiFlutterBridge.kt index 3a81a761..f15d4784 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/ConnectionUiFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/ConnectionUiFlutterBridge.kt @@ -15,16 +15,12 @@ import android.content.Intent import android.content.IntentFilter import android.content.IntentSender import android.os.Build -import android.service.notification.NotificationListenerService import io.rebble.cobble.BuildConfig import io.rebble.cobble.FlutterMainActivity -import io.rebble.cobble.MainActivity import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.notifications.NotificationListener import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.util.coroutines.asFlow -import io.rebble.cobble.util.hasNotificationAccessPermission +import io.rebble.cobble.shared.util.coroutines.asFlow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt index 5f8409d1..0587c27f 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt @@ -4,10 +4,10 @@ import android.content.Context import android.net.Uri import androidx.core.net.toFile import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.datasources.WatchMetadataStore import io.rebble.cobble.shared.middleware.PutBytesController import io.rebble.cobble.pigeons.BooleanWrapper import io.rebble.cobble.pigeons.Pigeons +import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.util.launchPigeonResult import io.rebble.cobble.shared.util.zippedSource import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform @@ -32,7 +32,6 @@ import javax.inject.Inject class FirmwareUpdateControlFlutterBridge @Inject constructor( bridgeLifecycleController: BridgeLifecycleController, private val coroutineScope: CoroutineScope, - private val watchMetadataStore: WatchMetadataStore, private val systemService: SystemService, private val putBytesController: PutBytesController, private val context: Context @@ -63,7 +62,7 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( } val hardwarePlatformNumber = withTimeoutOrNull(2_000) { - watchMetadataStore.lastConnectedWatchMetadata.first { it != null } + ConnectionStateManager.connectedWatchMetadata.first { it != null } } ?.running ?.hardwarePlatform @@ -129,7 +128,7 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( } val lastConnectedWatch = withTimeoutOrNull(2_000) { - watchMetadataStore.lastConnectedWatchMetadata.first { it != null } + ConnectionStateManager.connectedWatchMetadata.first { it != null } } ?: error("Watch not connected") diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt index 6beda558..8114a8a6 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt @@ -13,8 +13,8 @@ import androidx.core.content.getSystemService import androidx.lifecycle.Lifecycle import io.rebble.cobble.FlutterMainActivity import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.datasources.PermissionChangeBus -import io.rebble.cobble.notifications.NotificationListener +import io.rebble.cobble.shared.domain.PermissionChangeBus +import io.rebble.cobble.shared.domain.notifications.NotificationListener import io.rebble.cobble.pigeons.NumberWrapper import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.util.asFlow diff --git a/android/app/src/main/kotlin/io/rebble/cobble/datasources/PermissionChangeBus.kt b/android/app/src/main/kotlin/io/rebble/cobble/datasources/PermissionChangeBus.kt deleted file mode 100644 index ec0a6dfa..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/datasources/PermissionChangeBus.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.rebble.cobble.datasources - -import android.content.Context -import io.rebble.cobble.util.hasNotificationAccessPermission -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.flow.* - -/** - * Bus that triggers whenever permissions change - */ -@OptIn(ExperimentalCoroutinesApi::class) -object PermissionChangeBus { - private val permissionChangeChannel = BroadcastChannel(Channel.CONFLATED) - - fun openSubscription(): ReceiveChannel { - return permissionChangeChannel.openSubscription() - } - - fun trigger() { - permissionChangeChannel.trySend(Unit).isSuccess - } -} - -fun PermissionChangeBus.notificationPermissionFlow(context: Context): Flow { - return (openSubscription().consumeAsFlow().onStart { emit(Unit) }) - .map { context.hasNotificationAccessPermission() } - .distinctUntilChanged() -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/datasources/WatchMetadataStore.kt b/android/app/src/main/kotlin/io/rebble/cobble/datasources/WatchMetadataStore.kt deleted file mode 100644 index 6937efef..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/datasources/WatchMetadataStore.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.rebble.cobble.datasources - -import io.rebble.libpebblecommon.packets.WatchVersion -import kotlinx.coroutines.flow.MutableStateFlow -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton - -//TODO: Consolidate this with the shared ConnectionStateManager -@Singleton -class WatchMetadataStore @Inject constructor() { - val lastConnectedWatchMetadata = MutableStateFlow(null) - val lastConnectedWatchModel = MutableStateFlow(null) - val currentActiveApp = MutableStateFlow(null) -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt index c274bb6b..26b64a8e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt @@ -10,7 +10,6 @@ import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bluetooth.DeviceTransport import io.rebble.cobble.datasources.FlutterPreferences import io.rebble.cobble.datasources.PairedStorage -import io.rebble.cobble.datasources.WatchMetadataStore import io.rebble.cobble.errors.GlobalExceptionHandler import io.rebble.cobble.middleware.AppLogController import io.rebble.cobble.middleware.DeviceLogController @@ -41,7 +40,6 @@ interface AppComponent { fun createProtocolHandler(): ProtocolHandler fun createExceptionHandler(): GlobalExceptionHandler fun createConnectionLooper(): ConnectionLooper - fun createWatchMetadataStore(): WatchMetadataStore fun createPairedStorage(): PairedStorage fun createNotificationProcessor(): NotificationProcessor fun createCallNotificationProcessor(): CallNotificationProcessor diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt index 4e61664d..d2a9f734 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt @@ -4,12 +4,10 @@ import dagger.Binds import dagger.Module import dagger.Provides import dagger.multibindings.IntoSet -import io.rebble.cobble.handlers.* -import io.rebble.cobble.handlers.music.MusicHandler -import io.rebble.cobble.service.WatchService +import io.rebble.cobble.shared.handlers.music.MusicHandler import io.rebble.cobble.shared.domain.notifications.NotificationActionHandler import io.rebble.cobble.shared.handlers.AppInstallHandler -import io.rebble.cobble.shared.handlers.CalendarActionHandler +import io.rebble.cobble.shared.handlers.CalendarHandler import io.rebble.cobble.shared.handlers.CobbleHandler import kotlinx.coroutines.CoroutineScope import javax.inject.Named @@ -18,18 +16,10 @@ import javax.inject.Named abstract class ServiceModule { @Module companion object { - @Provides - fun provideCoroutineScope(watchService: WatchService): CoroutineScope { - return watchService.watchConnectionScope - } @Provides fun provideNotificationActionHandler(scope: CoroutineScope): NotificationActionHandler { return NotificationActionHandler(scope) } - @Provides - fun provideAppInstallHandler(scope: CoroutineScope): AppInstallHandler { - return AppInstallHandler(scope) - } } //TODO: Move to per-protocol handler services @@ -42,13 +32,6 @@ abstract class ServiceModule { ): CobbleHandler */ - @Binds - @IntoSet - @Named("negotiation") - abstract fun bindSystemMessageHandlerIntoSet( - systemMessageHandler: SystemHandler - ): CobbleHandler - @Binds @IntoSet @Named("normal") @@ -62,13 +45,6 @@ abstract class ServiceModule { notificationHandler: NotificationActionHandler ): CobbleHandler - @Binds - @IntoSet - @Named("normal") - abstract fun bindCalendarActionHandlerIntoSet( - calendarActionHandler: CalendarActionHandler - ): CobbleHandler - @Binds @IntoSet @Named("normal") diff --git a/android/app/src/main/kotlin/io/rebble/cobble/handlers/AppMessageHandler.kt b/android/app/src/main/kotlin/io/rebble/cobble/handlers/AppMessageHandler.kt deleted file mode 100644 index a1c1caea..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/handlers/AppMessageHandler.kt +++ /dev/null @@ -1,250 +0,0 @@ -package io.rebble.cobble.handlers - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import com.getpebble.android.kit.Constants -import com.getpebble.android.kit.util.PebbleDictionary -import io.rebble.cobble.datasources.WatchMetadataStore -import io.rebble.cobble.middleware.getPebbleDictionary -import io.rebble.cobble.middleware.toPacket -import io.rebble.cobble.shared.handlers.CobbleHandler -import io.rebble.cobble.util.coroutines.asFlow -import io.rebble.cobble.util.getIntExtraOrNull -import io.rebble.libpebblecommon.packets.AppCustomizationSetStockAppTitleMessage -import io.rebble.libpebblecommon.packets.AppMessage -import io.rebble.libpebblecommon.packets.AppRunStateMessage -import io.rebble.libpebblecommon.packets.AppType -import io.rebble.libpebblecommon.services.app.AppRunStateService -import io.rebble.libpebblecommon.services.appmessage.AppMessageService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.launch -import timber.log.Timber -import java.util.UUID -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -@OptIn(ExperimentalUnsignedTypes::class, ExperimentalStdlibApi::class) -@SuppressLint("BinaryOperationInTimber") -class AppMessageHandler @Inject constructor( - private val context: Context, - private val appMessageService: AppMessageService, - private val appRunStateService: AppRunStateService, - private val coroutineScope: CoroutineScope, - private val watchMetadataStore: WatchMetadataStore -) : CobbleHandler { - private var lastReceivedMessage: AppMessageTimestamp? = null - - init { - listenForIncomingPackets() - - listenForOutgoingDataMessages() - listenForOutgoingAckMessages() - listenForOutgoingNackMessages() - - listenForOutgoingAppStartMessages() - listenForOutgoingAppStopMessages() - - listenForOutgoingAppCustomizeMessages() - - sendConnectDisconnectIntents() - } - - private fun sendPushIntent(message: AppMessage.AppMessagePush) { - lastReceivedMessage = AppMessageTimestamp(message.uuid.get(), System.currentTimeMillis()) - - val intent = Intent(Constants.INTENT_APP_RECEIVE).apply { - putExtra(Constants.APP_UUID, message.uuid.get()) - putExtra(Constants.TRANSACTION_ID, message.transactionId.get().toInt()) - - val dictionary = message.getPebbleDictionary() - putExtra(Constants.MSG_DATA, dictionary.toJsonString()) - } - context.sendBroadcast(intent) - } - - private fun sendAckIntent(message: AppMessage.AppMessageACK) { - val intent = Intent(Constants.INTENT_APP_RECEIVE_ACK).apply { - putExtra(Constants.TRANSACTION_ID, message.transactionId.get().toInt()) - } - - context.sendBroadcast(intent) - } - - private fun sendNackIntent(transactionId: Int) { - val intent = Intent(Constants.INTENT_APP_RECEIVE_NACK).apply { - putExtra(Constants.TRANSACTION_ID, transactionId) - } - - context.sendBroadcast(intent) - } - - private fun listenForIncomingPackets() { - coroutineScope.launch { - for (message in appMessageService.receivedMessages) { - when (message) { - is AppMessage.AppMessagePush -> { - sendPushIntent(message) - } - - is AppMessage.AppMessageACK -> { - sendAckIntent(message) - } - - is AppMessage.AppMessageNACK -> { - sendNackIntent(message.transactionId.get().toInt()) - } - } - } - } - } - - private fun listenForOutgoingDataMessages() { - coroutineScope.launch { - IntentFilter(Constants.INTENT_APP_SEND).asFlow(context).collect { intent -> - val uuid = intent.getSerializableExtra(Constants.APP_UUID) as UUID - val transactionId: Int = intent.getIntExtra(Constants.TRANSACTION_ID, 0) - - if (!isAppActive(uuid)) { - sendNackIntent(transactionId) - return@collect - } - - val dictionary: PebbleDictionary = PebbleDictionary.fromJson( - intent.getStringExtra(Constants.MSG_DATA) - ) - - val packet = dictionary.toPacket(uuid, transactionId) - - appMessageService.send(packet) - } - } - } - - private fun listenForOutgoingAckMessages() { - coroutineScope.launch { - IntentFilter(Constants.INTENT_APP_ACK).asFlow(context).collect { intent -> - - val transactionId: Int = intent.getIntExtra(Constants.TRANSACTION_ID, 0) - - val packet = AppMessage.AppMessageACK( - transactionId.toUByte() - ) - - appMessageService.send(packet) - } - - } - } - - private fun listenForOutgoingNackMessages() { - coroutineScope.launch { - IntentFilter(Constants.INTENT_APP_NACK).asFlow(context).collect { intent -> - val transactionId: Int = intent.getIntExtra(Constants.TRANSACTION_ID, 0) - - val packet = AppMessage.AppMessageNACK( - transactionId.toUByte() - ) - - appMessageService.send(packet) - } - } - } - - private fun listenForOutgoingAppStartMessages() { - coroutineScope.launch { - IntentFilter(Constants.INTENT_APP_START).asFlow(context).collect { intent -> - val uuid = intent.getSerializableExtra(Constants.APP_UUID) as UUID - val packet = AppRunStateMessage.AppRunStateStart(uuid) - appRunStateService.send(packet) - } - } - } - - private fun listenForOutgoingAppStopMessages() { - coroutineScope.launch { - IntentFilter(Constants.INTENT_APP_STOP).asFlow(context).collect { intent -> - val uuid = intent.getSerializableExtra(Constants.APP_UUID) as UUID - val packet = AppRunStateMessage.AppRunStateStop(uuid) - appRunStateService.send(packet) - } - } - } - - private fun listenForOutgoingAppCustomizeMessages() { - coroutineScope.launch { - IntentFilter(Constants.INTENT_APP_CUSTOMIZE).asFlow(context).collect { intent -> - val appType = intent.getIntExtraOrNull(Constants.CUST_APP_TYPE) - ?.let { - if (it == 0) { - AppType.SPORTS - } else { - AppType.GOLF - } - } - ?: return@collect - - val name = intent.getStringExtra(Constants.CUST_NAME) ?: return@collect - - appMessageService.send( - AppCustomizationSetStockAppTitleMessage(appType, name) - ) - - // Pebble watch is also supposed to support customizing the icon of the - // sports/golf app, but this does not appear to work, even with the stock app - // maybe it was removed during later firmware upgrades? - - // Packets are there, but they do not work. Let's comment this until/if we - // ever figure this one out or if RebbleOS fixes it. - -// val image = intent.getParcelableExtra(Constants.CUST_ICON) ?: return@collect -// -// appMessageService.send( -// AppCustomizationSetStockAppIconMessage( -// appType, -// io.rebble.libpebblecommon.util.Bitmap(image) -// ) -// ) - } - } - } - - private fun sendConnectDisconnectIntents() { - coroutineScope.launch { - try { - context.sendBroadcast(Intent(Constants.INTENT_PEBBLE_CONNECTED)) - awaitCancellation() - } finally { - context.sendBroadcast(Intent(Constants.INTENT_PEBBLE_DISCONNECTED)) - } - } - } - - private fun isAppActive(app: UUID): Boolean { - val lastReceivedMessage = lastReceivedMessage - - return if (watchMetadataStore.currentActiveApp.value == app) { - true - } else if (lastReceivedMessage != null && - lastReceivedMessage.app == app && - (System.currentTimeMillis() - lastReceivedMessage.timestamp - ) < TimeUnit.SECONDS.toMillis(5)) { - // Sometimes app run state packets arrive with a delay. If we received incoming - // AppMessage from the app within last 5 seconds, consider it active - // and permit sending messages - true - } else { - Timber.w("Invalid AppMessage intent. " + - "Wanted to send a message to the app %s, but %s is active on the watch.", - app, - watchMetadataStore.currentActiveApp.value - ) - - false - } - } -} - -private data class AppMessageTimestamp(val app: UUID, val timestamp: Long) \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/handlers/AppRunStateHandler.kt b/android/app/src/main/kotlin/io/rebble/cobble/handlers/AppRunStateHandler.kt deleted file mode 100644 index 94ecc0a3..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/handlers/AppRunStateHandler.kt +++ /dev/null @@ -1,41 +0,0 @@ -package io.rebble.cobble.handlers - -import io.rebble.cobble.datasources.WatchMetadataStore -import io.rebble.cobble.shared.handlers.CobbleHandler -import io.rebble.libpebblecommon.packets.AppRunStateMessage -import io.rebble.libpebblecommon.services.app.AppRunStateService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import javax.inject.Inject - -class AppRunStateHandler @Inject constructor( - coroutineScope: CoroutineScope, - private val appRunStateService: AppRunStateService, - private val watchMetadataStore: WatchMetadataStore -) : CobbleHandler { - init { - coroutineScope.launch { listenForAppStateChanges() } - } - - private suspend fun listenForAppStateChanges() { - try { - for (message in appRunStateService.receivedMessages) { - when (message) { - is AppRunStateMessage.AppRunStateStart -> { - watchMetadataStore.currentActiveApp.value = message.uuid.get() - } - - is AppRunStateMessage.AppRunStateStop -> { - watchMetadataStore.currentActiveApp.value = null - } - - is AppRunStateMessage.AppRunStateRequest -> { - // Not supported - } - } - } - } finally { - watchMetadataStore.currentActiveApp.value = null - } - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt b/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt deleted file mode 100644 index 6b0203be..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt +++ /dev/null @@ -1,163 +0,0 @@ -package io.rebble.cobble.handlers - -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.hardware.Sensor -import android.hardware.SensorManager -import android.location.LocationManager -import androidx.core.content.ContextCompat.getSystemService -import io.rebble.cobble.bluetooth.ConnectionLooper -import io.rebble.cobble.datasources.WatchMetadataStore -import io.rebble.cobble.shared.domain.state.ConnectionState -import io.rebble.cobble.shared.domain.state.ConnectionStateManager -import io.rebble.cobble.shared.domain.state.watchOrNull -import io.rebble.cobble.shared.handlers.CobbleHandler -import io.rebble.cobble.util.coroutines.asFlow -import io.rebble.libpebblecommon.PacketPriority -import io.rebble.libpebblecommon.packets.PhoneAppVersion -import io.rebble.libpebblecommon.packets.ProtocolCapsFlag -import io.rebble.libpebblecommon.packets.TimeMessage -import io.rebble.libpebblecommon.services.SystemService -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.merge -import timber.log.Timber -import java.util.TimeZone -import javax.inject.Inject - -@OptIn(ExperimentalUnsignedTypes::class, ExperimentalStdlibApi::class) -class SystemHandler @Inject constructor( - private val context: Context, - private val coroutineScope: CoroutineScope, - private val systemService: SystemService, - private val connectionLooper: ConnectionLooper, - private val watchMetadataStore: WatchMetadataStore -) : CobbleHandler { - init { - systemService.appVersionRequestHandler = this::handleAppVersionRequest - listenForTimeChange() - - coroutineScope.launch { - // Wait until watch is connected before sending time - connectionLooper.connectionState.first { it is ConnectionState.Connected } - - sendCurrentTime() - } - - negotiate() - } - - fun negotiate() { - coroutineScope.launch { - connectionLooper.connectionState.first { it is ConnectionState.Negotiating } - Timber.i("Negotiating with watch") - try { - refreshWatchMetadata() - watchMetadataStore.lastConnectedWatchMetadata.value?.let { - if (it.running.isRecovery.get()) { - Timber.i("Watch is in recovery mode, switching to recovery state") - connectionLooper.connectionState.value.watchOrNull?.let { it1 -> connectionLooper.recoveryMode(it1) } - } else { - connectionLooper.connectionState.value.watchOrNull?.let { it1 -> connectionLooper.negotiationsComplete(it1) } - } - } - awaitCancellation() - } finally { - watchMetadataStore.lastConnectedWatchMetadata.value = null - watchMetadataStore.lastConnectedWatchModel.value = null - } - } - } - - private suspend fun refreshWatchMetadata() { - var retries = 0 - while (retries < 3) { - try { - withTimeout(3000) { - val watchInfo = systemService.requestWatchVersion() - //FIXME: Possible race condition here - ConnectionStateManager.connectionState.value.watchOrNull?.metadata?.value = watchInfo - watchMetadataStore.lastConnectedWatchMetadata.value = watchInfo - val watchModel = systemService.requestWatchModel() - watchMetadataStore.lastConnectedWatchModel.value = watchModel - } - break - } catch (e: TimeoutCancellationException) { - Timber.e(e, "Failed to get watch metadata, retrying") - retries++ - } catch (e: Exception) { - Timber.e(e, "Failed to get watch metadata") - break - } - } - if (retries >= 3) { - Timber.e("Failed to get watch metadata after 3 retries, giving up and reconnecting") - connectionLooper.tryReconnect() - } - } - - - @OptIn(ExperimentalCoroutinesApi::class) - private fun listenForTimeChange() { - val timeChangeFlow = IntentFilter(Intent.ACTION_TIME_CHANGED).asFlow(context) - val timezoneChangeFlow = IntentFilter(Intent.ACTION_TIMEZONE_CHANGED).asFlow(context) - - val mergedFlow = merge(timeChangeFlow, timezoneChangeFlow) - - coroutineScope.launch { - mergedFlow.collect { - sendCurrentTime() - } - } - } - - private suspend fun sendCurrentTime() { - val timezone = TimeZone.getDefault() - val now = System.currentTimeMillis() - - val updateTimePacket = TimeMessage.SetUTC( - (now / 1000).toUInt(), - timezone.getOffset(now).toShort(), - timezone.id - ) - - systemService.send(updateTimePacket, PacketPriority.LOW) - } - - private suspend fun handleAppVersionRequest(): PhoneAppVersion.AppVersionResponse { - val sensorManager = getSystemService(context, SensorManager::class.java) - val platflormFlags = mutableListOf(PhoneAppVersion.PlatformFlag.BTLE) - if (!sensorManager?.getSensorList(Sensor.TYPE_ACCELEROMETER).isNullOrEmpty()) platflormFlags.add(PhoneAppVersion.PlatformFlag.Accelerometer) - if (!sensorManager?.getSensorList(Sensor.TYPE_GYROSCOPE).isNullOrEmpty()) platflormFlags.add(PhoneAppVersion.PlatformFlag.Gyroscope) - if (!sensorManager?.getSensorList(Sensor.TYPE_MAGNETIC_FIELD).isNullOrEmpty()) platflormFlags.add(PhoneAppVersion.PlatformFlag.Compass) - - val locationManager = getSystemService(context, LocationManager::class.java) - if (locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) == true || locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) == true) platflormFlags.add(PhoneAppVersion.PlatformFlag.GPS) - - //TODO: check phone and sms capabilities - platflormFlags.add(PhoneAppVersion.PlatformFlag.Telephony) - - return PhoneAppVersion.AppVersionResponse( - UInt.MAX_VALUE, - 0u, - PhoneAppVersion.PlatformFlag.makeFlags( - PhoneAppVersion.OSType.Android, - platflormFlags - ), - 2u, - 4u, - 4u, - 2u, - ProtocolCapsFlag.makeFlags( - listOf( - ProtocolCapsFlag.Supports8kAppMessage, - ProtocolCapsFlag.SupportsExtendedMusicProtocol, - ProtocolCapsFlag.SupportsTwoWayDismissal, - ProtocolCapsFlag.SupportsAppRunStateProtocol - ) - ) - - ) - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt index fc9f4b82..7605083e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt @@ -6,12 +6,11 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build -import android.service.notification.NotificationListenerService import androidx.core.content.FileProvider import io.rebble.cobble.BuildConfig import io.rebble.cobble.CobbleApplication import io.rebble.cobble.middleware.DeviceLogController -import io.rebble.cobble.util.hasNotificationAccessPermission +import io.rebble.cobble.shared.util.hasNotificationAccessPermission import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import io.rebble.libpebblecommon.packets.LogDump import kotlinx.coroutines.Dispatchers diff --git a/android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt b/android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt index 28eabfd3..9fd82291 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt @@ -8,8 +8,8 @@ import android.net.Uri import com.getpebble.android.kit.Constants import io.rebble.cobble.CobbleApplication import io.rebble.cobble.bluetooth.ConnectionLooper -import io.rebble.cobble.datasources.WatchMetadataStore import io.rebble.cobble.shared.domain.state.ConnectionState +import io.rebble.cobble.shared.domain.state.ConnectionStateManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -20,7 +20,6 @@ class PebbleKitProvider : ContentProvider() { private var initialized = false private lateinit var connectionLooper: ConnectionLooper - private lateinit var watchMetadataStore: WatchMetadataStore override fun onCreate(): Boolean { // Do not initialize anything here as this gets called before Application.onCreate @@ -43,7 +42,6 @@ class PebbleKitProvider : ContentProvider() { .component connectionLooper = injectionComponent.createConnectionLooper() - watchMetadataStore = injectionComponent.createWatchMetadataStore() GlobalScope.launch(Dispatchers.Main.immediate) { connectionLooper.connectionState.collect { @@ -67,7 +65,7 @@ class PebbleKitProvider : ContentProvider() { val cursor = MatrixCursor(CURSOR_COLUMN_NAMES) - val metadata = watchMetadataStore.lastConnectedWatchMetadata.value + val metadata = ConnectionStateManager.connectedWatchMetadata.value if (connectionLooper.connectionState.value is ConnectionState.Connected && metadata != null) { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt index fb48b082..c6c68f27 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt @@ -2,13 +2,12 @@ package io.rebble.cobble.service import android.content.Context import android.content.Intent -import android.os.Build import android.service.notification.NotificationListenerService import androidx.core.content.ContextCompat import io.rebble.cobble.bluetooth.ConnectionLooper -import io.rebble.cobble.notifications.NotificationListener +import io.rebble.cobble.shared.domain.notifications.NotificationListener import io.rebble.cobble.shared.domain.state.ConnectionState -import io.rebble.cobble.util.hasNotificationAccessPermission +import io.rebble.cobble.shared.util.hasNotificationAccessPermission import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch diff --git a/android/app/src/test/java/io/rebble/cobble/middleware/PebbleDictionaryConverterTest.kt b/android/app/src/test/java/io/rebble/cobble/middleware/PebbleDictionaryConverterTest.kt index 160efaa4..9f03bb9c 100644 --- a/android/app/src/test/java/io/rebble/cobble/middleware/PebbleDictionaryConverterTest.kt +++ b/android/app/src/test/java/io/rebble/cobble/middleware/PebbleDictionaryConverterTest.kt @@ -1,5 +1,7 @@ package io.rebble.cobble.middleware +import io.rebble.cobble.shared.util.getPebbleDictionary +import io.rebble.cobble.shared.util.toPacket import io.rebble.libpebblecommon.packets.AppMessage import io.rebble.libpebblecommon.packets.AppMessageTuple import org.junit.Assert.assertEquals diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index ab5d0314..f5acdb48 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -2,6 +2,7 @@ activityCompose = "1.9.2" android-minSdk = "29" android-targetSdk = "34" +androidxVersion = "1.13.1" coroutinesVersion = "1.8.1" daggerVersion = "2.51.1" gradle = "8.5.1" @@ -12,6 +13,7 @@ kotlinxSerializationJson = "1.7.1" ksp = "2.0.20-Beta2-1.0.23" libpebblecommonVersion = "0.1.24" errorproneVersion = "2.26.1" +rruleVersion = "1.0.3" spotbugsVersion = "4.8.6" protoliteWellKnownTypes = "18.0.0" @@ -19,6 +21,7 @@ room = "2.7.0-alpha08" room-sqlite = "2.5.0-alpha08" datastore = "1.1.1" androidxTest = "1.6.1" +timberVersion = "5.0.1" uuidVersion = "0.8.4" compose-lib = "1.7.0-beta02" compose-nav = "2.7.0-alpha07" @@ -27,6 +30,7 @@ reorderable = "2.3.3" appcompat = "1.7.0" ktorVersion = "2.3.12" +workManagerVersion = "2.9.0" [plugins] multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } @@ -43,14 +47,17 @@ jetbrains-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", versi [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxVersion" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxTest" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTest" } androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidxTest" } +androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workManagerVersion" } dagger = { module = "com.google.dagger:dagger", version.ref = "daggerVersion" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "daggerVersion" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } kgp = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "errorproneVersion" } +rrule = { module = "com.github.PhilJay:RRule", version.ref = "rruleVersion" } spotbugs-annotations = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "spotbugsVersion" } androidx-sqlite = { module = "androidx.sqlite:sqlite", version.ref = "room-sqlite" } @@ -70,6 +77,7 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version. kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationJson" } libpebblecommon = { module = "io.rebble.libpebblecommon:libpebblecommon", version.ref = "libpebblecommonVersion" } +timber = { module = "com.jakewharton.timber:timber", version.ref = "timberVersion" } uuid = { module = "com.benasher44:uuid", version.ref = "uuidVersion" } protolite-wellknowntypes = { module = "com.google.firebase:protolite-well-known-types", version.ref = "protoliteWellKnownTypes" } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt index a2983caa..8ab14abb 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt @@ -5,8 +5,10 @@ import android.bluetooth.BluetoothDevice import androidx.annotation.RequiresPermission import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.ProtocolHandler +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.isActive interface BlueIO { @FlowPreview @@ -21,7 +23,7 @@ class BluetoothPebbleDevice( ) : PebbleDevice(null, protocolHandler, address){ override fun toString(): String { - val start = "< BluetoothPebbleDevice, address=$address, bluetoothDevice=< BluetoothDevice address=${bluetoothDevice.address}" + val start = "< BluetoothPebbleDevice, address=$address, connectionScopeActive=${connectionScope.value?.isActive}, bluetoothDevice=< BluetoothDevice address=${bluetoothDevice.address}" return try { "$start, name=${bluetoothDevice.name}, type=${bluetoothDevice.type} > >" } catch (e: SecurityException) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt index 7fb7f6c2..838f4173 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt @@ -3,6 +3,8 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothDevice import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.ProtocolHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.isActive class EmulatedPebbleDevice( address: String, @@ -10,6 +12,6 @@ class EmulatedPebbleDevice( ) : PebbleDevice(null, protocolHandler, address){ override fun toString(): String { - return "< EmulatedPebbleDevice, address=$address >" + return "< EmulatedPebbleDevice, address=$address, connectionScopeActive=${connectionScope.value?.isActive} >" } } \ No newline at end of file diff --git a/android/shared/build.gradle.kts b/android/shared/build.gradle.kts index 1a0f6d36..c14ab7b4 100644 --- a/android/shared/build.gradle.kts +++ b/android/shared/build.gradle.kts @@ -72,9 +72,10 @@ kotlin { androidMain.dependencies { implementation(libs.ktor.client.okhttp) implementation(libs.koin.android) - implementation("androidx.core:core-ktx:$androidxVersion") - implementation("com.jakewharton.timber:timber:$timberVersion") - implementation("com.github.PhilJay:RRule:$rruleVersion") + implementation(libs.androidx.work.runtime.ktx) + implementation(libs.androidx.core.ktx) + implementation(libs.timber) + implementation(libs.rrule) } commonTest.dependencies { implementation(kotlin("test")) diff --git a/android/app/src/main/java/com/getpebble/android/kit/Constants.java b/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/Constants.java similarity index 100% rename from android/app/src/main/java/com/getpebble/android/kit/Constants.java rename to android/shared/src/androidMain/kotlin/com/getpebble/android/kit/Constants.java diff --git a/android/app/src/main/java/com/getpebble/android/kit/LICENSE b/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/LICENSE similarity index 100% rename from android/app/src/main/java/com/getpebble/android/kit/LICENSE rename to android/shared/src/androidMain/kotlin/com/getpebble/android/kit/LICENSE diff --git a/android/app/src/main/java/com/getpebble/android/kit/PebbleKit.java b/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/PebbleKit.java similarity index 84% rename from android/app/src/main/java/com/getpebble/android/kit/PebbleKit.java rename to android/shared/src/androidMain/kotlin/com/getpebble/android/kit/PebbleKit.java index 2a0e2645..cc97d028 100644 --- a/android/app/src/main/java/com/getpebble/android/kit/PebbleKit.java +++ b/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/PebbleKit.java @@ -9,49 +9,12 @@ import android.util.Base64; import android.util.Log; -import com.getpebble.android.kit.Constants.PebbleAppType; -import com.getpebble.android.kit.Constants.PebbleDataType; import com.getpebble.android.kit.util.PebbleDictionary; import org.json.JSONException; import java.util.UUID; -import static com.getpebble.android.kit.Constants.APP_UUID; -import static com.getpebble.android.kit.Constants.CUST_APP_TYPE; -import static com.getpebble.android.kit.Constants.CUST_ICON; -import static com.getpebble.android.kit.Constants.CUST_NAME; -import static com.getpebble.android.kit.Constants.DATA_LOG_TAG; -import static com.getpebble.android.kit.Constants.DATA_LOG_TIMESTAMP; -import static com.getpebble.android.kit.Constants.DATA_LOG_UUID; -import static com.getpebble.android.kit.Constants.INTENT_APP_ACK; -import static com.getpebble.android.kit.Constants.INTENT_APP_CUSTOMIZE; -import static com.getpebble.android.kit.Constants.INTENT_APP_NACK; -import static com.getpebble.android.kit.Constants.INTENT_APP_RECEIVE; -import static com.getpebble.android.kit.Constants.INTENT_APP_RECEIVE_ACK; -import static com.getpebble.android.kit.Constants.INTENT_APP_RECEIVE_NACK; -import static com.getpebble.android.kit.Constants.INTENT_APP_SEND; -import static com.getpebble.android.kit.Constants.INTENT_APP_START; -import static com.getpebble.android.kit.Constants.INTENT_APP_STOP; -import static com.getpebble.android.kit.Constants.INTENT_DL_ACK_DATA; -import static com.getpebble.android.kit.Constants.INTENT_DL_FINISH_SESSION; -import static com.getpebble.android.kit.Constants.INTENT_DL_RECEIVE_DATA; -import static com.getpebble.android.kit.Constants.INTENT_DL_REQUEST_DATA; -import static com.getpebble.android.kit.Constants.INTENT_PEBBLE_CONNECTED; -import static com.getpebble.android.kit.Constants.INTENT_PEBBLE_DISCONNECTED; -import static com.getpebble.android.kit.Constants.KIT_STATE_COLUMN_APPMSG_SUPPORT; -import static com.getpebble.android.kit.Constants.KIT_STATE_COLUMN_CONNECTED; -import static com.getpebble.android.kit.Constants.KIT_STATE_COLUMN_DATALOGGING_SUPPORT; -import static com.getpebble.android.kit.Constants.KIT_STATE_COLUMN_VERSION_MAJOR; -import static com.getpebble.android.kit.Constants.KIT_STATE_COLUMN_VERSION_MINOR; -import static com.getpebble.android.kit.Constants.KIT_STATE_COLUMN_VERSION_POINT; -import static com.getpebble.android.kit.Constants.KIT_STATE_COLUMN_VERSION_TAG; -import static com.getpebble.android.kit.Constants.MSG_DATA; -import static com.getpebble.android.kit.Constants.PBL_DATA_ID; -import static com.getpebble.android.kit.Constants.PBL_DATA_OBJECT; -import static com.getpebble.android.kit.Constants.PBL_DATA_TYPE; -import static com.getpebble.android.kit.Constants.TRANSACTION_ID; - /** * A helper class providing methods for interacting with third-party Pebble Smartwatch applications. Pebble-enabled * Android applications may use this class to assist in sending/receiving data between the watch and the phone. @@ -85,10 +48,10 @@ private PebbleKit() { * @param name The custom name to be applied to the watch-app. Names must be less than 32 characters in length. * @param icon The custom icon to be applied to the watch-app. Icons must be black-and-white bitmaps no larger than 32px * in either dimension. - * @throws IllegalArgumentException Thrown if the specified name or icon are invalid. {@link PebbleAppType#SPORTS} or {@link - * PebbleAppType#GOLF}. + * @throws IllegalArgumentException Thrown if the specified name or icon are invalid. {@link Constants.PebbleAppType#SPORTS} or {@link + * Constants.PebbleAppType#GOLF}. */ - public static void customizeWatchApp(final Context context, final PebbleAppType appType, + public static void customizeWatchApp(final Context context, final Constants.PebbleAppType appType, final String name, final Bitmap icon) throws IllegalArgumentException { if (appType == null) { @@ -106,10 +69,10 @@ public static void customizeWatchApp(final Context context, final PebbleAppType icon.getWidth(), icon.getHeight())); } - final Intent customizeAppIntent = new Intent(INTENT_APP_CUSTOMIZE); - customizeAppIntent.putExtra(CUST_APP_TYPE, appType.ord); - customizeAppIntent.putExtra(CUST_NAME, name); - customizeAppIntent.putExtra(CUST_ICON, icon); + final Intent customizeAppIntent = new Intent(Constants.INTENT_APP_CUSTOMIZE); + customizeAppIntent.putExtra(Constants.CUST_APP_TYPE, appType.ord); + customizeAppIntent.putExtra(Constants.CUST_NAME, name); + customizeAppIntent.putExtra(Constants.CUST_ICON, icon); context.sendBroadcast(customizeAppIntent); } @@ -129,7 +92,7 @@ public static boolean isWatchConnected(final Context context) { if (c == null || !c.moveToNext()) { return false; } - return c.getInt(KIT_STATE_COLUMN_CONNECTED) == 1; + return c.getInt(Constants.KIT_STATE_COLUMN_CONNECTED) == 1; } finally { if (c != null) { c.close(); @@ -154,7 +117,7 @@ public static boolean areAppMessagesSupported(final Context context) { if (c == null || !c.moveToNext()) { return false; } - return c.getInt(KIT_STATE_COLUMN_APPMSG_SUPPORT) == 1; + return c.getInt(Constants.KIT_STATE_COLUMN_APPMSG_SUPPORT) == 1; } finally { if (c != null) { c.close(); @@ -180,10 +143,10 @@ public static FirmwareVersionInfo getWatchFWVersion(final Context context) { return null; } - int majorVersion = c.getInt(KIT_STATE_COLUMN_VERSION_MAJOR); - int minorVersion = c.getInt(KIT_STATE_COLUMN_VERSION_MINOR); - int pointVersion = c.getInt(KIT_STATE_COLUMN_VERSION_POINT); - String versionTag = c.getString(KIT_STATE_COLUMN_VERSION_TAG); + int majorVersion = c.getInt(Constants.KIT_STATE_COLUMN_VERSION_MAJOR); + int minorVersion = c.getInt(Constants.KIT_STATE_COLUMN_VERSION_MINOR); + int pointVersion = c.getInt(Constants.KIT_STATE_COLUMN_VERSION_POINT); + String versionTag = c.getString(Constants.KIT_STATE_COLUMN_VERSION_TAG); return new FirmwareVersionInfo(majorVersion, minorVersion, pointVersion, versionTag); } finally { @@ -210,7 +173,7 @@ public static boolean isDataLoggingSupported(final Context context) { if (c == null || !c.moveToNext()) { return false; } - return c.getInt(KIT_STATE_COLUMN_DATALOGGING_SUPPORT) == 1; + return c.getInt(Constants.KIT_STATE_COLUMN_DATALOGGING_SUPPORT) == 1; } finally { if (c != null) { c.close(); @@ -234,8 +197,8 @@ public static void startAppOnPebble(final Context context, final UUID watchappUu throw new IllegalArgumentException("uuid cannot be null"); } - final Intent startAppIntent = new Intent(INTENT_APP_START); - startAppIntent.putExtra(APP_UUID, watchappUuid); + final Intent startAppIntent = new Intent(Constants.INTENT_APP_START); + startAppIntent.putExtra(Constants.APP_UUID, watchappUuid); context.sendBroadcast(startAppIntent); } @@ -255,8 +218,8 @@ public static void closeAppOnPebble(final Context context, final UUID watchappUu throw new IllegalArgumentException("uuid cannot be null"); } - final Intent stopAppIntent = new Intent(INTENT_APP_STOP); - stopAppIntent.putExtra(APP_UUID, watchappUuid); + final Intent stopAppIntent = new Intent(Constants.INTENT_APP_STOP); + stopAppIntent.putExtra(Constants.APP_UUID, watchappUuid); context.sendBroadcast(stopAppIntent); } @@ -311,10 +274,10 @@ public static void sendDataToPebbleWithTransactionId(final Context context, return; } - final Intent sendDataIntent = new Intent(INTENT_APP_SEND); - sendDataIntent.putExtra(APP_UUID, watchappUuid); - sendDataIntent.putExtra(TRANSACTION_ID, transactionId); - sendDataIntent.putExtra(MSG_DATA, data.toJsonString()); + final Intent sendDataIntent = new Intent(Constants.INTENT_APP_SEND); + sendDataIntent.putExtra(Constants.APP_UUID, watchappUuid); + sendDataIntent.putExtra(Constants.TRANSACTION_ID, transactionId); + sendDataIntent.putExtra(Constants.MSG_DATA, data.toJsonString()); context.sendBroadcast(sendDataIntent); } @@ -335,8 +298,8 @@ public static void sendAckToPebble(final Context context, final int transactionI "transaction id must be between (0, 255); got '%d'", transactionId)); } - final Intent ackIntent = new Intent(INTENT_APP_ACK); - ackIntent.putExtra(TRANSACTION_ID, transactionId); + final Intent ackIntent = new Intent(Constants.INTENT_APP_ACK); + ackIntent.putExtra(Constants.TRANSACTION_ID, transactionId); context.sendBroadcast(ackIntent); } @@ -357,8 +320,8 @@ public static void sendNackToPebble(final Context context, final int transaction "transaction id must be between (0, 255); got '%d'", transactionId)); } - final Intent nackIntent = new Intent(INTENT_APP_NACK); - nackIntent.putExtra(TRANSACTION_ID, transactionId); + final Intent nackIntent = new Intent(Constants.INTENT_APP_NACK); + nackIntent.putExtra(Constants.TRANSACTION_ID, transactionId); context.sendBroadcast(nackIntent); } @@ -375,7 +338,7 @@ public static void sendNackToPebble(final Context context, final int transaction */ public static BroadcastReceiver registerPebbleConnectedReceiver(final Context context, final BroadcastReceiver receiver) { - return registerBroadcastReceiverInternal(context, INTENT_PEBBLE_CONNECTED, receiver); + return registerBroadcastReceiverInternal(context, Constants.INTENT_PEBBLE_CONNECTED, receiver); } /** @@ -392,7 +355,7 @@ public static BroadcastReceiver registerPebbleConnectedReceiver(final Context co */ public static BroadcastReceiver registerPebbleDisconnectedReceiver(final Context context, final BroadcastReceiver receiver) { - return registerBroadcastReceiverInternal(context, INTENT_PEBBLE_DISCONNECTED, receiver); + return registerBroadcastReceiverInternal(context, Constants.INTENT_PEBBLE_DISCONNECTED, receiver); } /** @@ -408,7 +371,7 @@ public static BroadcastReceiver registerPebbleDisconnectedReceiver(final Context */ public static BroadcastReceiver registerReceivedDataHandler(final Context context, final PebbleDataReceiver receiver) { - return registerBroadcastReceiverInternal(context, INTENT_APP_RECEIVE, receiver); + return registerBroadcastReceiverInternal(context, Constants.INTENT_APP_RECEIVE, receiver); } @@ -426,7 +389,7 @@ public static BroadcastReceiver registerReceivedDataHandler(final Context contex */ public static BroadcastReceiver registerReceivedAckHandler(final Context context, final PebbleAckReceiver receiver) { - return registerBroadcastReceiverInternal(context, INTENT_APP_RECEIVE_ACK, receiver); + return registerBroadcastReceiverInternal(context, Constants.INTENT_APP_RECEIVE_ACK, receiver); } /** @@ -443,7 +406,7 @@ public static BroadcastReceiver registerReceivedAckHandler(final Context context */ public static BroadcastReceiver registerReceivedNackHandler(final Context context, final PebbleNackReceiver receiver) { - return registerBroadcastReceiverInternal(context, INTENT_APP_RECEIVE_NACK, receiver); + return registerBroadcastReceiverInternal(context, Constants.INTENT_APP_RECEIVE_NACK, receiver); } /** @@ -480,8 +443,8 @@ private static BroadcastReceiver registerBroadcastReceiverInternal(final Context public static BroadcastReceiver registerDataLogReceiver(final Context context, final PebbleDataLogReceiver receiver) { IntentFilter filter = new IntentFilter(); - filter.addAction(INTENT_DL_RECEIVE_DATA); - filter.addAction(INTENT_DL_FINISH_SESSION); + filter.addAction(Constants.INTENT_DL_RECEIVE_DATA); + filter.addAction(Constants.INTENT_DL_FINISH_SESSION); context.registerReceiver(receiver, filter); return receiver; @@ -500,8 +463,8 @@ public static BroadcastReceiver registerDataLogReceiver(final Context context, * @see Constants#INTENT_DL_REQUEST_DATA */ public static void requestDataLogsForApp(final Context context, final UUID appUuid) { - final Intent requestIntent = new Intent(INTENT_DL_REQUEST_DATA); - requestIntent.putExtra(APP_UUID, appUuid); + final Intent requestIntent = new Intent(Constants.INTENT_DL_REQUEST_DATA); + requestIntent.putExtra(Constants.APP_UUID, appUuid); context.sendBroadcast(requestIntent); } @@ -516,7 +479,7 @@ private static Cursor queryProvider(final Context context) { if (c != null) { if (c.moveToFirst()) { // If Basalt app is connected, talk to that (return the open cursor) - if (c.getInt(KIT_STATE_COLUMN_CONNECTED) == 1) { + if (c.getInt(Constants.KIT_STATE_COLUMN_CONNECTED) == 1) { c.moveToPrevious(); return c; } @@ -565,7 +528,7 @@ public abstract void receiveData(final Context context, final int transactionId, */ @Override public void onReceive(final Context context, final Intent intent) { - final UUID receivedUuid = (UUID) intent.getSerializableExtra(APP_UUID); + final UUID receivedUuid = (UUID) intent.getSerializableExtra(Constants.APP_UUID); // Pebble-enabled apps are expected to be good citizens and only inspect broadcasts // containing their UUID @@ -573,8 +536,8 @@ public void onReceive(final Context context, final Intent intent) { return; } - final int transactionId = intent.getIntExtra(TRANSACTION_ID, -1); - final String jsonData = intent.getStringExtra(MSG_DATA); + final int transactionId = intent.getIntExtra(Constants.TRANSACTION_ID, -1); + final String jsonData = intent.getStringExtra(Constants.MSG_DATA); if (jsonData == null || jsonData.isEmpty()) { return; } @@ -623,7 +586,7 @@ protected PebbleAckReceiver(final UUID subscribedUuid) { */ @Override public void onReceive(final Context context, final Intent intent) { - final int transactionId = intent.getIntExtra(TRANSACTION_ID, -1); + final int transactionId = intent.getIntExtra(Constants.TRANSACTION_ID, -1); receiveAck(context, transactionId); } @@ -663,7 +626,7 @@ protected PebbleNackReceiver(final UUID subscribedUuid) { */ @Override public void onReceive(final Context context, final Intent intent) { - final int transactionId = intent.getIntExtra(TRANSACTION_ID, -1); + final int transactionId = intent.getIntExtra(Constants.TRANSACTION_ID, -1); receiveNack(context, transactionId); } @@ -757,7 +720,7 @@ public void onFinishSession(final Context context, UUID logUuid, final Long time private void handleReceiveDataIntent(final Context context, final Intent intent, final UUID logUuid, final Long timestamp, final Long tag) { - final int dataId = intent.getIntExtra(PBL_DATA_ID, -1); + final int dataId = intent.getIntExtra(Constants.PBL_DATA_ID, -1); if (dataId < 0) throw new IllegalArgumentException(); Log.i("pebble", "DataID: " + dataId + " LastDataID: " + lastDataId); @@ -767,12 +730,12 @@ private void handleReceiveDataIntent(final Context context, final Intent intent, return; } - final PebbleDataType type = PebbleDataType.fromByte(intent.getByteExtra(PBL_DATA_TYPE, PebbleDataType.INVALID.ord)); + final Constants.PebbleDataType type = Constants.PebbleDataType.fromByte(intent.getByteExtra(Constants.PBL_DATA_TYPE, Constants.PebbleDataType.INVALID.ord)); if (type == null) throw new IllegalArgumentException(); switch (type) { case BYTES: - byte[] bytes = Base64.decode(intent.getStringExtra(PBL_DATA_OBJECT), Base64.NO_WRAP); + byte[] bytes = Base64.decode(intent.getStringExtra(Constants.PBL_DATA_OBJECT), Base64.NO_WRAP); if (bytes == null) { throw new IllegalArgumentException(); } @@ -780,7 +743,7 @@ private void handleReceiveDataIntent(final Context context, final Intent intent, receiveData(context, logUuid, timestamp, tag, bytes); break; case UINT: - Long uint = (Long) intent.getSerializableExtra(PBL_DATA_OBJECT); + Long uint = (Long) intent.getSerializableExtra(Constants.PBL_DATA_OBJECT); if (uint == null) { throw new IllegalArgumentException(); } @@ -788,7 +751,7 @@ private void handleReceiveDataIntent(final Context context, final Intent intent, receiveData(context, logUuid, timestamp, tag, uint); break; case INT: - Integer i = (Integer) intent.getSerializableExtra(PBL_DATA_OBJECT); + Integer i = (Integer) intent.getSerializableExtra(Constants.PBL_DATA_OBJECT); if (i == null) { throw new IllegalArgumentException(); } @@ -801,9 +764,9 @@ private void handleReceiveDataIntent(final Context context, final Intent intent, lastDataId = dataId; - final Intent ackIntent = new Intent(INTENT_DL_ACK_DATA); - ackIntent.putExtra(DATA_LOG_UUID, logUuid); - ackIntent.putExtra(PBL_DATA_ID, dataId); + final Intent ackIntent = new Intent(Constants.INTENT_DL_ACK_DATA); + ackIntent.putExtra(Constants.DATA_LOG_UUID, logUuid); + ackIntent.putExtra(Constants.PBL_DATA_ID, dataId); context.sendBroadcast(ackIntent); } @@ -817,7 +780,7 @@ private void handleFinishSessionIntent(final Context context, final Intent inten */ @Override public void onReceive(final Context context, final Intent intent) { - final UUID receivedUuid = (UUID) intent.getSerializableExtra(APP_UUID); + final UUID receivedUuid = (UUID) intent.getSerializableExtra(Constants.APP_UUID); // Pebble-enabled apps are expected to be good citizens and only inspect broadcasts // containing their UUID if (!subscribedUuid.equals(receivedUuid)) { @@ -829,18 +792,18 @@ public void onReceive(final Context context, final Intent intent) { final Long timestamp; final Long tag; - logUuid = (UUID) intent.getSerializableExtra(DATA_LOG_UUID); + logUuid = (UUID) intent.getSerializableExtra(Constants.DATA_LOG_UUID); if (logUuid == null) throw new IllegalArgumentException(); - timestamp = (Long) intent.getSerializableExtra(DATA_LOG_TIMESTAMP); + timestamp = (Long) intent.getSerializableExtra(Constants.DATA_LOG_TIMESTAMP); if (timestamp == null) throw new IllegalArgumentException(); - tag = (Long) intent.getSerializableExtra(DATA_LOG_TAG); + tag = (Long) intent.getSerializableExtra(Constants.DATA_LOG_TAG); if (tag == null) throw new IllegalArgumentException(); - if (intent.getAction() == INTENT_DL_RECEIVE_DATA) { + if (intent.getAction() == Constants.INTENT_DL_RECEIVE_DATA) { handleReceiveDataIntent(context, intent, logUuid, timestamp, tag); - } else if (intent.getAction() == INTENT_DL_FINISH_SESSION) { + } else if (intent.getAction() == Constants.INTENT_DL_FINISH_SESSION) { handleFinishSessionIntent(context, intent, logUuid, timestamp, tag); } } catch (IllegalArgumentException e) { diff --git a/android/app/src/main/java/com/getpebble/android/kit/util/PebbleDictionary.java b/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/PebbleDictionary.java similarity index 100% rename from android/app/src/main/java/com/getpebble/android/kit/util/PebbleDictionary.java rename to android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/PebbleDictionary.java diff --git a/android/app/src/main/java/com/getpebble/android/kit/util/PebbleTuple.java b/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/PebbleTuple.java similarity index 100% rename from android/app/src/main/java/com/getpebble/android/kit/util/PebbleTuple.java rename to android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/PebbleTuple.java diff --git a/android/app/src/main/java/com/getpebble/android/kit/util/SportsState.java b/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/SportsState.java similarity index 100% rename from android/app/src/main/java/com/getpebble/android/kit/util/SportsState.java rename to android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/SportsState.java diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/AndroidPlatformContext.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/AndroidPlatformContext.kt index 912794a3..41bc6a3f 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/AndroidPlatformContext.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/AndroidPlatformContext.kt @@ -1,5 +1,9 @@ package io.rebble.cobble.shared +import io.rebble.libpebblecommon.packets.PhoneAppVersion + class AndroidPlatformContext( val applicationContext: android.content.Context -) : PlatformContext \ No newline at end of file +) : PlatformContext { + override val osType: PhoneAppVersion.OSType = PhoneAppVersion.OSType.Android +} \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt index 287182f1..bf027a61 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt @@ -9,6 +9,9 @@ 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.handlers.CalendarHandler +import io.rebble.cobble.shared.handlers.CobbleHandler +import io.rebble.cobble.shared.handlers.music.MusicHandler import io.rebble.cobble.shared.jobs.AndroidJobScheduler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -30,4 +33,12 @@ val androidModule = module { single { AndroidJobScheduler() } singleOf(::AndroidNotificationActionExecutor) singleOf(::AndroidCalendarActionExecutor) + + factory>(named("deviceHandlers")) { params -> + inject(named("commonDeviceHandlers")).value + + setOf( + CalendarHandler(params.get()), + MusicHandler(params.get()) + ) + } } \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt new file mode 100644 index 00000000..cdb79080 --- /dev/null +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt @@ -0,0 +1,24 @@ +package io.rebble.cobble.shared.domain + +import android.content.Context +import io.rebble.cobble.shared.util.hasNotificationAccessPermission +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* + +/** + * Bus that triggers whenever permissions change + */ +object PermissionChangeBus { + private val _permissionChangeFlow = MutableSharedFlow(Channel.CONFLATED) + val permissionChangeFlow = _permissionChangeFlow.asSharedFlow() + + fun trigger() { + _permissionChangeFlow.tryEmit(Unit) + } +} + +fun PermissionChangeBus.notificationPermissionFlow(context: Context): Flow { + return (permissionChangeFlow.onStart { emit(Unit) }) + .map { context.hasNotificationAccessPermission() } + .distinctUntilChanged() +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationListener.kt similarity index 96% rename from android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationListener.kt index 098415fe..03fad559 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationListener.kt @@ -1,10 +1,9 @@ -package io.rebble.cobble.notifications +package io.rebble.cobble.shared.domain.notifications import android.app.Notification import android.app.NotificationChannel import android.content.ComponentName import android.content.Context -import android.icu.text.UnicodeSet import android.os.Build import android.os.UserHandle import android.service.notification.NotificationListenerService @@ -13,15 +12,11 @@ import androidx.core.app.NotificationCompat import com.benasher44.uuid.Uuid import io.rebble.cobble.CobbleApplication import io.rebble.cobble.bluetooth.ConnectionLooper -import io.rebble.cobble.data.NotificationMessage import io.rebble.cobble.data.toNotificationGroup import io.rebble.cobble.datasources.FlutterPreferences import io.rebble.cobble.shared.database.dao.NotificationChannelDao import io.rebble.cobble.shared.datastore.KMPPrefs import io.rebble.cobble.shared.domain.state.ConnectionState -import io.rebble.libpebblecommon.packets.blobdb.BlobResponse -import io.rebble.libpebblecommon.packets.blobdb.TimelineItem -import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber @@ -256,7 +251,7 @@ class NotificationListener : NotificationListenerService() { companion object { private val _isActive = MutableStateFlow(false) - val isActive: StateFlow by ::_isActive + val isActive: StateFlow by Companion::_isActive fun getComponentName(context: Context): ComponentName { return ComponentName(context, NotificationListener::class.java) diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AndroidPlatformAppMessageIPC.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AndroidPlatformAppMessageIPC.kt new file mode 100644 index 00000000..f5807397 --- /dev/null +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AndroidPlatformAppMessageIPC.kt @@ -0,0 +1,140 @@ +package io.rebble.cobble.shared.handlers + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.benasher44.uuid.uuidFrom +import com.getpebble.android.kit.Constants +import com.getpebble.android.kit.util.PebbleDictionary +import io.rebble.cobble.shared.util.coroutines.asFlow +import io.rebble.cobble.shared.util.getIntExtraOrNull +import io.rebble.cobble.shared.util.getPebbleDictionary +import io.rebble.cobble.shared.util.toPacket +import io.rebble.libpebblecommon.packets.AppCustomizationSetStockAppTitleMessage +import io.rebble.libpebblecommon.packets.AppMessage +import io.rebble.libpebblecommon.packets.AppType +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull + +class AndroidPlatformAppMessageIPC(private val context: Context): PlatformAppMessageIPC { + override fun sendPush(message: AppMessage.AppMessagePush) { + val intent = Intent(Constants.INTENT_APP_RECEIVE).apply { + putExtra(Constants.APP_UUID, message.uuid.get()) + putExtra(Constants.TRANSACTION_ID, message.transactionId.get().toInt()) + + val dictionary = message.getPebbleDictionary() + putExtra(Constants.MSG_DATA, dictionary.toJsonString()) + } + context.sendBroadcast(intent) + } + + override fun sendAck(message: AppMessage.AppMessageACK) { + val intent = Intent(Constants.INTENT_APP_RECEIVE_ACK).apply { + putExtra(Constants.TRANSACTION_ID, message.transactionId.get().toInt()) + } + + context.sendBroadcast(intent) + } + + override fun sendNack(transactionId: Int) { + val intent = Intent(Constants.INTENT_APP_RECEIVE_NACK).apply { + putExtra(Constants.TRANSACTION_ID, transactionId) + } + + context.sendBroadcast(intent) + } + + override fun outgoingMessages(): Flow { + val intentFilter = IntentFilter() + intentFilter.addAction(Constants.INTENT_APP_SEND) + intentFilter.addAction(Constants.INTENT_APP_ACK) + intentFilter.addAction(Constants.INTENT_APP_NACK) + intentFilter.addAction(Constants.INTENT_APP_START) + intentFilter.addAction(Constants.INTENT_APP_STOP) + intentFilter.addAction(Constants.INTENT_APP_CUSTOMIZE) + return intentFilter.asFlow(context).mapNotNull { + when (it.action) { + Constants.INTENT_APP_SEND -> { + val uuid = uuidFrom(it.getStringExtra(Constants.APP_UUID)!!) + val transactionId = it.getIntExtra(Constants.TRANSACTION_ID, 0) + val dictionary = PebbleDictionary.fromJson(it.getStringExtra(Constants.MSG_DATA)!!) + + OutgoingMessage.Data(uuid, transactionId, dictionary.toPacket(uuid, transactionId)) + } + Constants.INTENT_APP_ACK -> { + val transactionId: Int = it.getIntExtra(Constants.TRANSACTION_ID, 0) + + OutgoingMessage.Ack( + AppMessage.AppMessageACK( + transactionId.toUByte() + ) + ) + } + Constants.INTENT_APP_NACK -> { + val transactionId: Int = it.getIntExtra(Constants.TRANSACTION_ID, 0) + + OutgoingMessage.Nack( + AppMessage.AppMessageNACK( + transactionId.toUByte() + ) + ) + } + Constants.INTENT_APP_START -> { + val uuid = uuidFrom(it.getStringExtra(Constants.APP_UUID)!!) + + OutgoingMessage.AppStart(uuid) + } + Constants.INTENT_APP_STOP -> { + val uuid = uuidFrom(it.getStringExtra(Constants.APP_UUID)!!) + + OutgoingMessage.AppStop(uuid) + } + Constants.INTENT_APP_CUSTOMIZE -> { + val appType = it.getIntExtraOrNull(Constants.CUST_APP_TYPE) + ?.let { type -> + if (type == 0) { + AppType.SPORTS + } else { + AppType.GOLF + } + } + ?: return@mapNotNull null + + val name = it.getStringExtra(Constants.CUST_NAME) ?: return@mapNotNull null + + OutgoingMessage.AppCustomize( + appType, + name + ) + + // Pebble watch is also supposed to support customizing the icon of the + // sports/golf app, but this does not appear to work, even with the stock app + // maybe it was removed during later firmware upgrades? + + // Packets are there, but they do not work. Let's comment this until/if we + // ever figure this one out or if RebbleOS fixes it. + +// val image = intent.getParcelableExtra(Constants.CUST_ICON) ?: return@collect +// +// appMessageService.send( +// AppCustomizationSetStockAppIconMessage( +// appType, +// io.rebble.libpebblecommon.util.Bitmap(image) +// ) +// ) + } + else -> throw IllegalStateException("Unknown action: ${it.action}") + } + } + } + + override fun broadcastPebbleConnected() { + context.sendBroadcast(Intent(Constants.INTENT_PEBBLE_CONNECTED)) + } + + override fun broadcastPebbleDisconnected() { + context.sendBroadcast(Intent(Constants.INTENT_PEBBLE_DISCONNECTED)) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/handlers/CalendarHandler.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/CalendarHandler.kt similarity index 61% rename from android/app/src/main/kotlin/io/rebble/cobble/handlers/CalendarHandler.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/CalendarHandler.kt index 3630a5e1..35f5e854 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/handlers/CalendarHandler.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/CalendarHandler.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.handlers +package io.rebble.cobble.shared.handlers import android.Manifest import android.content.Context @@ -9,67 +9,63 @@ import android.provider.CalendarContract import androidx.core.content.ContextCompat import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager -import io.rebble.cobble.background.CalendarSyncWorker -import io.rebble.cobble.datasources.FlutterPreferences -import io.rebble.cobble.datasources.PermissionChangeBus import io.rebble.cobble.shared.datastore.KMPPrefs +import io.rebble.cobble.shared.domain.PermissionChangeBus import io.rebble.cobble.shared.domain.calendar.CalendarSync -import io.rebble.cobble.shared.handlers.CobbleHandler -import io.rebble.cobble.util.Debouncer -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.consumeAsFlow -import kotlinx.coroutines.flow.onStart +import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.cobble.shared.jobs.CalendarSyncWorker +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import timber.log.Timber import java.util.concurrent.TimeUnit -import javax.inject.Inject - -class CalendarHandler @Inject constructor( - private val context: Context, - private val coroutineScope: CoroutineScope, - private val calendarSync: CalendarSync, - private val flutterPreferences: FlutterPreferences, - private val prefs: KMPPrefs -) : CobbleHandler { + +private const val CALENDAR_WORK_TAG = "CalendarSync" + +class CalendarHandler(private val pebbleDevice: PebbleDevice) : CobbleHandler, KoinComponent { + private val calendarSync: CalendarSync by inject() + private val prefs: KMPPrefs by inject() + private val context: Context by inject() + private var initialSyncJob: Job? = null private var calendarHandlerStarted = false - val calendarDebouncer = Debouncer(debouncingTimeMs = 1_000, scope = coroutineScope) + private val calendarChangeFlow = MutableSharedFlow() private val contentObserver = object : ContentObserver(null) { override fun onChange(selfChange: Boolean, uri: Uri?) { - // Android seems to fire multiple calendar notifications at the same time. - // Use debouncer to wait for the last change and then trigger sync - calendarDebouncer.executeDebouncing { - Timber.d("Calendar change detected. Syncing...") - calendarSync.doFullCalendarSync() - Timber.d("Sync complete") - } + calendarChangeFlow.tryEmit(Unit) } } init { - val permissionChangeFlow = PermissionChangeBus.openSubscription() - .consumeAsFlow() + val permissionChangeFlow = PermissionChangeBus.permissionChangeFlow .onStart { emit(Unit) } - - coroutineScope.launch { - combine( - permissionChangeFlow, - prefs.calendarSyncEnabled - ) { _, - calendarSyncEnabled -> - startStopCalendar(calendarSyncEnabled) - }.collect() - } + combine( + permissionChangeFlow, + prefs.calendarSyncEnabled + ) { _, + calendarSyncEnabled -> + startStopCalendar(calendarSyncEnabled) + }.launchIn(pebbleDevice.negotiationScope) + + calendarChangeFlow + .debounce(1000) + .onEach { + Timber.d("Calendar change detected. Syncing...") + calendarSync.doFullCalendarSync() + } + .launchIn(pebbleDevice.negotiationScope) } private fun startStopCalendar(calendarSyncEnabled: Boolean) { val hasPermission = ContextCompat.checkSelfPermission( - context, - Manifest.permission.READ_CALENDAR + context, + Manifest.permission.READ_CALENDAR ) == PackageManager.PERMISSION_GRANTED synchronized(this) { val shouldSyncCalendar = hasPermission && calendarSyncEnabled @@ -89,7 +85,7 @@ class CalendarHandler @Inject constructor( observeCalendarChanges() schedulePeriodicCalendarSync() - initialSyncJob = coroutineScope.launch { + initialSyncJob = pebbleDevice.negotiationScope.launch { // We were not receiving any calendar changes when service was offline or we did not // have permissions. // Sync calendar @@ -99,7 +95,7 @@ class CalendarHandler @Inject constructor( Timber.d("Sync complete") } - coroutineScope.coroutineContext.job.invokeOnCompletion { + pebbleDevice.negotiationScope.coroutineContext.job.invokeOnCompletion { stopCalendarHandler() } } @@ -125,10 +121,8 @@ class CalendarHandler @Inject constructor( private fun observeCalendarChanges() { context.contentResolver.registerContentObserver( - CalendarContract.Instances.CONTENT_URI, true, contentObserver + CalendarContract.Instances.CONTENT_URI, true, contentObserver ) } -} - -private const val CALENDAR_WORK_TAG = "CalendarSync" \ No newline at end of file +} \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.android.kt new file mode 100644 index 00000000..479cc98c --- /dev/null +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.android.kt @@ -0,0 +1,46 @@ +package io.rebble.cobble.shared.handlers + +import android.content.Intent +import android.content.IntentFilter +import android.hardware.Sensor +import android.hardware.SensorManager +import android.location.LocationManager +import androidx.core.content.ContextCompat.getSystemService +import io.rebble.cobble.shared.AndroidPlatformContext +import io.rebble.cobble.shared.PlatformContext +import io.rebble.cobble.shared.util.coroutines.asFlow +import io.rebble.libpebblecommon.packets.PhoneAppVersion +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +actual fun platformTimeChangedFlow(context: PlatformContext): Flow { + context as AndroidPlatformContext + val timeChangeFlow = IntentFilter(Intent.ACTION_TIME_CHANGED).asFlow(context.applicationContext) + val timezoneChangeFlow = IntentFilter(Intent.ACTION_TIMEZONE_CHANGED).asFlow(context.applicationContext) + + return merge(timeChangeFlow, timezoneChangeFlow).map { } +} + +actual fun getPlatformPebbleFlags(context: PlatformContext): Set { + context as AndroidPlatformContext + val sensorManager = getSystemService(context.applicationContext, SensorManager::class.java) + val locationManager = getSystemService(context.applicationContext, LocationManager::class.java) + + return buildSet { + add(PhoneAppVersion.PlatformFlag.BTLE) + //TODO: Check for SMS + add(PhoneAppVersion.PlatformFlag.Telephony) + + if (!sensorManager?.getSensorList(Sensor.TYPE_ACCELEROMETER).isNullOrEmpty()) add(PhoneAppVersion.PlatformFlag.Accelerometer) + if (!sensorManager?.getSensorList(Sensor.TYPE_GYROSCOPE).isNullOrEmpty()) add(PhoneAppVersion.PlatformFlag.Gyroscope) + if (!sensorManager?.getSensorList(Sensor.TYPE_MAGNETIC_FIELD).isNullOrEmpty()) add(PhoneAppVersion.PlatformFlag.Compass) + if ( + locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) == true || + locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) == true + ) { + add(PhoneAppVersion.PlatformFlag.GPS) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/handlers/music/ActiveMediaSessionProvider.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/ActiveMediaSessionProvider.kt similarity index 94% rename from android/app/src/main/kotlin/io/rebble/cobble/handlers/music/ActiveMediaSessionProvider.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/ActiveMediaSessionProvider.kt index b86c88ab..6a6b038c 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/handlers/music/ActiveMediaSessionProvider.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/ActiveMediaSessionProvider.kt @@ -1,17 +1,19 @@ -package io.rebble.cobble.handlers.music +package io.rebble.cobble.shared.handlers.music import android.content.Context import android.media.session.MediaController import android.media.session.MediaSessionManager import android.media.session.PlaybackState import io.rebble.cobble.notifications.NotificationListener +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import timber.log.Timber import javax.inject.Inject -class ActiveMediaSessionProvider @Inject constructor(private val context: Context) : +class ActiveMediaSessionProvider : androidx.lifecycle.LiveData(), - MediaSessionManager.OnActiveSessionsChangedListener { - + MediaSessionManager.OnActiveSessionsChangedListener, KoinComponent { + private val context: Context by inject() private val mediaSessionManager: MediaSessionManager = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager var currentController: MediaController? = null diff --git a/android/app/src/main/kotlin/io/rebble/cobble/handlers/music/MusicHandler.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/MusicHandler.kt similarity index 80% rename from android/app/src/main/kotlin/io/rebble/cobble/handlers/music/MusicHandler.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/MusicHandler.kt index 6c4ddbc9..62b0e19d 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/handlers/music/MusicHandler.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/MusicHandler.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.handlers.music +package io.rebble.cobble.shared.handlers.music import android.content.Context import android.content.pm.PackageManager @@ -9,50 +9,54 @@ import android.media.session.PlaybackState import android.os.SystemClock import android.view.KeyEvent import androidx.lifecycle.asFlow -import io.rebble.cobble.datasources.PermissionChangeBus -import io.rebble.cobble.datasources.notificationPermissionFlow +import io.rebble.cobble.shared.domain.PermissionChangeBus +import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.cobble.shared.domain.notificationPermissionFlow import io.rebble.cobble.shared.handlers.CobbleHandler -import io.rebble.cobble.util.Debouncer import io.rebble.libpebblecommon.packets.MusicControl import io.rebble.libpebblecommon.services.MusicService import kotlinx.coroutines.* import kotlinx.coroutines.channels.actor import kotlinx.coroutines.flow.* +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import timber.log.Timber -import javax.inject.Inject import kotlin.math.roundToInt -class MusicHandler @Inject constructor( - private val context: Context, - private val coroutineScope: CoroutineScope, - private val musicService: MusicService, - private val activeMediaSessionProvider: ActiveMediaSessionProvider, - private val packageManager: PackageManager -) : CobbleHandler { +class MusicHandler(private val pebbleDevice: PebbleDevice): CobbleHandler, KoinComponent { + private val context: Context by inject() + private val packageManager: PackageManager by inject() + private val activeMediaSessionProvider = ActiveMediaSessionProvider() + private val musicService = pebbleDevice.musicService + private var currentMediaController: MediaController? = null private var hasPermission: Boolean = false private val musicControl = MutableSharedFlow(4) init { - musicControl.filterIsInstance().debounce(200).onEach { - Timber.d("Update current track %s %s %s %s", it.title.get(), it.artist.get(), it.album.get(), it.trackLength.get()) - musicService.send(it) - }.launchIn(coroutineScope) - - musicControl.filterIsInstance().debounce(200).onEach { - Timber.d("Update play state %s %s %s", it.state.get(), it.trackPosition.get(), it.playRate.get()) - musicService.send(it) - }.launchIn(coroutineScope) - - musicControl.filterIsInstance().debounce(200).onEach { - Timber.d("Update volume %s", it.volumePercent.get()) - musicService.send(it) - }.launchIn(coroutineScope) - musicControl.filterIsInstance().debounce(200).onEach { - Timber.d("Update player info %s %s", it.name.get(), it.pkg.get()) - musicService.send(it) - }.launchIn(coroutineScope) + pebbleDevice.negotiationScope.launch { + val connectionScope = pebbleDevice.connectionScope.filterNotNull().first() + + musicControl.filterIsInstance().debounce(200).onEach { + Timber.d("Update current track %s %s %s %s", it.title.get(), it.artist.get(), it.album.get(), it.trackLength.get()) + musicService.send(it) + }.launchIn(connectionScope) + + musicControl.filterIsInstance().debounce(200).onEach { + Timber.d("Update play state %s %s %s", it.state.get(), it.trackPosition.get(), it.playRate.get()) + musicService.send(it) + }.launchIn(connectionScope) + + musicControl.filterIsInstance().debounce(200).onEach { + Timber.d("Update volume %s", it.volumePercent.get()) + musicService.send(it) + }.launchIn(connectionScope) + musicControl.filterIsInstance().debounce(200).onEach { + Timber.d("Update player info %s %s", it.name.get(), it.pkg.get()) + musicService.send(it) + }.launchIn(connectionScope) + } } private fun onMediaPlayerChanged(newPlayer: MediaController?) { @@ -202,8 +206,8 @@ class MusicHandler @Inject constructor( } } - private fun listenForPlayerChanges() { - coroutineScope.launch(Dispatchers.Main.immediate) { + private fun listenForPlayerChanges(connectionScope: CoroutineScope) { + connectionScope.launch(Dispatchers.Main.immediate) { @Suppress("EXPERIMENTAL_API_USAGE") PermissionChangeBus.notificationPermissionFlow(context) .flatMapLatest { hasNotificationPermission -> @@ -221,8 +225,8 @@ class MusicHandler @Inject constructor( } } - private fun listenForIncomingMessages() { - coroutineScope.launch(Dispatchers.Main.immediate) { + private fun listenForIncomingMessages(connectionScope: CoroutineScope) { + connectionScope.launch(Dispatchers.Main.immediate) { for (msg in musicService.receivedMessages) { Timber.d("Received music packet %s %s", msg.message, currentMediaController?.packageName) when (msg.message) { @@ -296,11 +300,13 @@ class MusicHandler @Inject constructor( } init { - listenForIncomingMessages() - listenForPlayerChanges() - - coroutineScope.coroutineContext.job.invokeOnCompletion { - disposeCurrentMediaController() + pebbleDevice.negotiationScope.launch { + val connectionScope = pebbleDevice.connectionScope.filterNotNull().first() + listenForIncomingMessages(connectionScope) + listenForPlayerChanges(connectionScope) + connectionScope.coroutineContext.job.invokeOnCompletion { + disposeCurrentMediaController() + } } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/background/CalendarSyncWorker.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/jobs/CalendarSyncWorker.kt similarity index 72% rename from android/app/src/main/kotlin/io/rebble/cobble/background/CalendarSyncWorker.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/jobs/CalendarSyncWorker.kt index ba891ee6..aeb0e805 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/background/CalendarSyncWorker.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/jobs/CalendarSyncWorker.kt @@ -1,17 +1,19 @@ -package io.rebble.cobble.background +package io.rebble.cobble.shared.jobs import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import io.rebble.cobble.CobbleApplication +import io.rebble.cobble.shared.domain.calendar.CalendarSync import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import timber.log.Timber -class CalendarSyncWorker(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) { +class CalendarSyncWorker(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params), KoinComponent { + private val calendarSync: CalendarSync by inject() override suspend fun doWork(): Result { Timber.d("Calendar sync worker start") - val component = (applicationContext as CobbleApplication).component if (ConnectionStateManager.connectionState.value is ConnectionState.Disconnected) { // Periodic syncs are only needed when watch service is active @@ -22,7 +24,7 @@ class CalendarSyncWorker(appContext: Context, params: WorkerParameters) : Corout return Result.success() } - return if (component.createKMPCalendarSync().doFullCalendarSync()) { + return if (calendarSync.doFullCalendarSync()) { Timber.d("Success") Result.success() } else { diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt index 8db12a60..30d7653b 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt @@ -11,7 +11,7 @@ actual object JsRunnerFactory: KoinComponent { private val context: Context by inject() actual fun createJsRunner( scope: CoroutineScope, - connectedAddress: StateFlow, + connectedAddress: String, appInfo: PbwAppInfo, jsPath: String ): JsRunner = WebViewJsRunner(context, connectedAddress, scope, appInfo, jsPath) diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt index 0a390be5..e1f47c90 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt @@ -18,7 +18,7 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -class WebViewJsRunner(val context: Context, private val connectedAddress: StateFlow, private val scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner(appInfo, jsPath) { +class WebViewJsRunner(val context: Context, private val connectedAddress: String, private val scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner(appInfo, jsPath) { companion object { const val API_NAMESPACE = "Pebble" @@ -238,7 +238,7 @@ class WebViewJsRunner(val context: Context, private val connectedAddress: StateF } suspend fun signalReady() { - val readyDeviceIds = listOf(connectedAddress.value ?: return) + val readyDeviceIds = listOf(connectedAddress) val readyJson = Json.encodeToString(readyDeviceIds) withContext(Dispatchers.Main) { webView?.loadUrl("javascript:signalReady(${Uri.encode(readyJson)})") diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.android.kt index 84c9014b..73af612a 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.android.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.android.kt @@ -1,11 +1,15 @@ package io.rebble.cobble.shared.util +import com.benasher44.uuid.Uuid +import io.rebble.cobble.shared.AndroidPlatformContext +import io.rebble.cobble.shared.PlatformContext import io.rebble.libpebblecommon.metadata.WatchType import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwManifest import kotlinx.serialization.json.decodeFromStream import okio.Source import okio.buffer +import okio.source actual fun getPbwManifest( pbwFile: File, @@ -56,5 +60,23 @@ actual fun requirePbwAppInfo(pbwFile: File): PbwAppInfo { */ actual fun requirePbwBinaryBlob(pbwFile: File, watchType: WatchType, blobName: String): Source { return pbwFile.zippedPlatformSource(watchType, blobName) - ?: error("Blob ${blobName} missing from app $pbwFile") + ?: error("Blob $blobName missing from app $pbwFile") +} + +actual fun getPbwJsFilePath(context: PlatformContext, pbwAppInfo: PbwAppInfo, pbwFile: File): String? { + context as AndroidPlatformContext + val cache = context.applicationContext.cacheDir.resolve("js") + cache.mkdirs() + val cachedJsFile = cache.resolve("${pbwAppInfo.uuid}-${pbwAppInfo.versionCode}.js") + if (cachedJsFile.exists()) { + return cachedJsFile.absolutePath + } + val jsFile = pbwFile.zippedSource("pebble-js-app.js") + ?: return null + + cachedJsFile.bufferedWriter().use { + it.write(jsFile.buffer().readUtf8()) + } + + return cachedJsFile.absolutePath } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/util/Intent.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/Intent.kt similarity index 82% rename from android/app/src/main/kotlin/io/rebble/cobble/util/Intent.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/Intent.kt index 24960358..d7bdf066 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/util/Intent.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/Intent.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.util +package io.rebble.cobble.shared.util import android.content.Intent diff --git a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PebbleDictionaryConverter.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/PebbleDictionaryConverter.kt similarity index 99% rename from android/app/src/main/kotlin/io/rebble/cobble/middleware/PebbleDictionaryConverter.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/PebbleDictionaryConverter.kt index 0607687c..4c74e203 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PebbleDictionaryConverter.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/PebbleDictionaryConverter.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.middleware +package io.rebble.cobble.shared.util import com.getpebble.android.kit.util.PebbleDictionary import com.getpebble.android.kit.util.PebbleTuple diff --git a/android/app/src/main/kotlin/io/rebble/cobble/util/Permissions.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/Permissions.kt similarity index 97% rename from android/app/src/main/kotlin/io/rebble/cobble/util/Permissions.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/Permissions.kt index c7756087..94ab3504 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/util/Permissions.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/Permissions.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.util +package io.rebble.cobble.shared.util import android.content.Context import android.os.Build diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/ZipUtils.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/ZipUtils.kt index add8e999..30ffcf5d 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/ZipUtils.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/ZipUtils.kt @@ -48,6 +48,8 @@ fun File.zippedSource(fileName: String): Source? { fun io.rebble.cobble.shared.util.File.zippedSource(fileName: String): Source? = this.file.zippedSource(fileName) +fun io.rebble.cobble.shared.util.File.source(): Source = this.file.source() + fun InputStream.zippedSource(fileName: String): Source? { val zipInputStream = ZipInputStream(this) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/util/coroutines/BroadcastReceiver.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/coroutines/BroadcastReceiver.kt similarity index 96% rename from android/app/src/main/kotlin/io/rebble/cobble/util/coroutines/BroadcastReceiver.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/coroutines/BroadcastReceiver.kt index e17dd1f8..44db9ce3 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/util/coroutines/BroadcastReceiver.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/coroutines/BroadcastReceiver.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.util.coroutines +package io.rebble.cobble.shared.util.coroutines import android.content.BroadcastReceiver import android.content.Context diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/PlatformContext.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/PlatformContext.kt index b02b2640..577f7fe2 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/PlatformContext.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/PlatformContext.kt @@ -1,4 +1,7 @@ package io.rebble.cobble.shared +import io.rebble.libpebblecommon.packets.PhoneAppVersion + interface PlatformContext { + val osType: PhoneAppVersion.OSType } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt index 53279924..a41f4976 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt @@ -4,8 +4,11 @@ import io.rebble.cobble.shared.middleware.PutBytesController import io.rebble.libpebblecommon.ProtocolHandlerImpl import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.services.AppFetchService +import io.rebble.libpebblecommon.services.MusicService import io.rebble.libpebblecommon.services.PutBytesService +import io.rebble.libpebblecommon.services.SystemService import io.rebble.libpebblecommon.services.app.AppRunStateService +import io.rebble.libpebblecommon.services.appmessage.AppMessageService import io.rebble.libpebblecommon.services.blobdb.BlobDBService import org.koin.dsl.module import io.rebble.libpebblecommon.services.blobdb.TimelineService @@ -24,7 +27,6 @@ val libpebbleModule = module { single { AppFetchService(get()) } - single { PutBytesController() } @@ -36,4 +38,16 @@ val libpebbleModule = module { factory { params -> BlobDBService(params.get()) } + + factory { params -> + AppMessageService(params.get()) + } + + factory { params -> + MusicService(params.get()) + } + + factory { params -> + SystemService(params.get()) + } } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/PebbleDeviceModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/PebbleDeviceModule.kt new file mode 100644 index 00000000..d56a68bf --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/PebbleDeviceModule.kt @@ -0,0 +1,23 @@ +package io.rebble.cobble.shared.di + +import io.rebble.cobble.shared.handlers.* +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val pebbleDeviceModule = module { + + factory>(named("commonNegotiationDeviceHandlers")) { params -> + setOf( + SystemHandler(params.get()), + ) + } + factory>(named("commonDeviceHandlers")) { params -> + get>(named("commonNegotiationDeviceHandlers")) + + setOf( + SystemHandler(params.get()), + AppRunStateHandler(params.get()), + AppInstallHandler(params.get()), + CalendarActionHandler(params.get()) + ) + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt index 9f726b38..9bd5a92d 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt @@ -26,9 +26,6 @@ val stateModule = module { .map { it is ConnectionState.Connected } .stateIn(CoroutineScope(Dispatchers.Default), SharingStarted.WhileSubscribed(), false) } - single(named("connectionScope")) { - MutableStateFlow(CoroutineScope(EmptyCoroutineContext)) - } bind StateFlow::class single(named("currentToken")) { MutableStateFlow(CurrentToken.LoggedOut) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/CalendarSync.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/CalendarSync.kt index 7b6d4631..c82a21ca 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/CalendarSync.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/CalendarSync.kt @@ -25,7 +25,6 @@ class CalendarSync( ): AutoCloseable, KoinComponent { private val calendarSyncer: PhoneCalendarSyncer by inject() private val connectionState: StateFlow by inject(named("connectionState")) - private val connectionScope: StateFlow by inject(named("connectionScope")) private val timelinePinDao: TimelinePinDao by inject() private val calendarDao: CalendarDao by inject() private val calendarEnableChangeFlow: MutableSharedFlow> = MutableSharedFlow() @@ -36,7 +35,7 @@ class CalendarSync( private val watchConnectedListener = connectionState.filterIsInstance().onEach { Logging.d("Watch connected, syncing calendar pins") - connectionScope.value.launch { + it.watch.connectionScope.value!!.launch { val res = onWatchConnected(it.watch.metadata.filterNotNull().first().isUnfaithful.get() ?: false, it.watch) Logging.d("Calendar sync result: $res") } @@ -83,16 +82,22 @@ class CalendarSync( calendarSyncer.clearAllCalendarsFromDb() calendarSyncer.syncDeviceCalendarsToDb() - connectionState.value.watchOrNull?.blobDBService?.let { - val watchTimelineSyncer = WatchTimelineSyncer(it) - watchTimelineSyncer.clearAllPinsFromWatchAndResync() + connectionState.value.watchOrNull?.let { + val connectionScope = it.connectionScope.value + connectionScope?.async { + val watchTimelineSyncer = WatchTimelineSyncer(it.blobDBService) + watchTimelineSyncer.clearAllPinsFromWatchAndResync() + }?.await() } } private suspend fun syncTimelineToWatch(): Boolean { - connectionState.value.watchOrNull?.blobDBService?.let { - val watchTimelineSyncer = WatchTimelineSyncer(it) - return watchTimelineSyncer.syncPinDatabaseWithWatch() + connectionState.value.watchOrNull?.let { + val connectionScope = it.connectionScope.value + return connectionScope?.async { + val watchTimelineSyncer = WatchTimelineSyncer(it.blobDBService) + return@async watchTimelineSyncer.syncPinDatabaseWithWatch() + }?.await() ?: false } ?: return false } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt index d3a7f402..06c1bb2f 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt @@ -1,10 +1,19 @@ package io.rebble.cobble.shared.domain.common +import com.benasher44.uuid.Uuid import io.ktor.http.parametersOf +import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.packets.WatchVersion +import io.rebble.libpebblecommon.services.MusicService +import io.rebble.libpebblecommon.services.SystemService import io.rebble.libpebblecommon.services.app.AppRunStateService +import io.rebble.libpebblecommon.services.appmessage.AppMessageService import io.rebble.libpebblecommon.services.blobdb.BlobDBService +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -13,13 +22,24 @@ import org.koin.core.parameter.parametersOf open class PebbleDevice( metadata: WatchVersion.WatchVersionResponse?, private val protocolHandler: ProtocolHandler, - val address: String -): KoinComponent { + val address: String, +): KoinComponent, AutoCloseable { + val negotiationScope = CoroutineScope(Dispatchers.Default + CoroutineName("NegotationScope-$address")) val metadata: MutableStateFlow = MutableStateFlow(metadata) + val modelId: MutableStateFlow = MutableStateFlow(null) + val connectionScope: MutableStateFlow = MutableStateFlow(null) + val currentActiveApp: MutableStateFlow = MutableStateFlow(null) override fun toString(): String = "< PebbleDevice address=$address >" //TODO: Move to per-protocol handler services, so we can have multiple PebbleDevices, this is the first of many val appRunStateService: AppRunStateService by inject {parametersOf(protocolHandler)} val blobDBService: BlobDBService by inject {parametersOf(protocolHandler)} + val appMessageService: AppMessageService by inject {parametersOf(protocolHandler)} + val systemService: SystemService by inject {parametersOf(protocolHandler)} + val musicService: MusicService by inject {parametersOf(protocolHandler)} + + override fun close() { + negotiationScope.cancel("PebbleDevice closed") + } } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt index 819bd33e..347349e4 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt @@ -30,7 +30,6 @@ val ConnectionState.watchOrNull: PebbleDevice? object ConnectionStateManager: KoinComponent { val connectionState: MutableStateFlow by inject(named("connectionState")) - val connectionScope: MutableStateFlow by inject(named("connectionScope")) /** * Flow of the currently connected watch's metadata. This flow only emits when a watch is connected and will not emit if negotiation never completes. diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt index f3937df7..b0f8b5b8 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt @@ -10,6 +10,7 @@ import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.api.RWS import io.rebble.cobble.shared.database.dao.LockerDao +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.middleware.PutBytesController import io.rebble.cobble.shared.util.AppCompatibility.getBestVariant @@ -21,6 +22,8 @@ import io.rebble.libpebblecommon.packets.AppFetchResponse import io.rebble.libpebblecommon.packets.AppFetchResponseStatus import io.rebble.libpebblecommon.services.AppFetchService import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull @@ -28,7 +31,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject class AppInstallHandler( - coroutineScope: CoroutineScope + pebbleDevice: PebbleDevice ): CobbleHandler, KoinComponent { private val lockerDao: LockerDao by inject() private val platformContext: PlatformContext by inject() @@ -37,10 +40,13 @@ class AppInstallHandler( private val putBytesController: PutBytesController by inject() init { - coroutineScope.launch { - for (message in appFetchService.receivedMessages) { - when (message) { - is AppFetchRequest -> onNewAppFetchRequest(message) + pebbleDevice.negotiationScope.launch { + val deviceScope = pebbleDevice.connectionScope.filterNotNull().first() + deviceScope.launch { + for (message in appFetchService.receivedMessages) { + when (message) { + is AppFetchRequest -> onNewAppFetchRequest(message) + } } } } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt new file mode 100644 index 00000000..ff070e20 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt @@ -0,0 +1,167 @@ +package io.rebble.cobble.shared.handlers + +import com.benasher44.uuid.Uuid +import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.PlatformContext +import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.libpebblecommon.packets.AppCustomizationSetStockAppTitleMessage +import io.rebble.libpebblecommon.packets.AppMessage +import io.rebble.libpebblecommon.packets.AppRunStateMessage +import io.rebble.libpebblecommon.packets.AppType +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket +import io.rebble.libpebblecommon.services.app.AppRunStateService +import io.rebble.libpebblecommon.services.appmessage.AppMessageService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.parameter.parametersOf +import kotlin.time.Duration.Companion.seconds + +private data class AppMessageTimestamp(val app: Uuid, val timestamp: Long) + + + +open class OutgoingMessage { + data class Data( + val uuid: Uuid, + val transactionId: Int, + val packet: AppMessage + ) : OutgoingMessage() + + data class Ack( + val packet: AppMessage + ) : OutgoingMessage() + + data class Nack( + val packet: AppMessage + ) : OutgoingMessage() + + data class AppStart( + val uuid: Uuid + ) : OutgoingMessage() + + data class AppStop( + val uuid: Uuid + ) : OutgoingMessage() + + data class AppCustomize( + val appType: AppType, + val name: String + ) : OutgoingMessage() +} + +interface PlatformAppMessageIPC { + fun sendPush(message: AppMessage.AppMessagePush) + fun sendAck(message: AppMessage.AppMessageACK) + fun sendNack(transactionId: Int) + fun outgoingMessages(): Flow + fun broadcastPebbleConnected() + fun broadcastPebbleDisconnected() +} + +class AppMessageHandler( + private val pebbleDevice: PebbleDevice, +) : KoinComponent, CobbleHandler { + private val platformAppMessageIPC: PlatformAppMessageIPC by inject() + private var lastReceivedMessage: AppMessageTimestamp? = null + private val outgoingMessages = platformAppMessageIPC.outgoingMessages().buffer() + + init { + pebbleDevice.negotiationScope.launch { + val deviceScope = pebbleDevice.connectionScope.filterNotNull().first() + listenForIncomingPackets(deviceScope) + + listenForOutgoingMessages(deviceScope) + + sendConnectDisconnectEvents(deviceScope) + } + + } + + private fun listenForIncomingPackets(deviceScope: CoroutineScope) { + deviceScope.launch { + for (message in pebbleDevice.appMessageService.receivedMessages) { + when (message) { + is AppMessage.AppMessagePush -> { + lastReceivedMessage = AppMessageTimestamp(message.uuid.get(), Clock.System.now().toEpochMilliseconds()) + platformAppMessageIPC.sendPush(message) + } + + is AppMessage.AppMessageACK -> { + platformAppMessageIPC.sendAck(message) + } + + is AppMessage.AppMessageNACK -> { + platformAppMessageIPC.sendNack(message.transactionId.get().toInt()) + } + } + } + } + } + + private fun listenForOutgoingMessages(deviceScope: CoroutineScope) { + outgoingMessages.buffer().onEach { + when (it) { + is OutgoingMessage.Data -> { + if (!isAppActive(it.uuid)) { + platformAppMessageIPC.sendNack(it.transactionId) + } + pebbleDevice.appMessageService.send(it.packet) + } + is OutgoingMessage.Ack -> { + pebbleDevice.appMessageService.send(it.packet) + } + is OutgoingMessage.Nack -> { + pebbleDevice.appMessageService.send(it.packet) + } + is OutgoingMessage.AppStart -> { + pebbleDevice.appRunStateService.startApp(it.uuid) + } + is OutgoingMessage.AppStop -> { + pebbleDevice.appRunStateService.stopApp(it.uuid) + } + is OutgoingMessage.AppCustomize -> { + pebbleDevice.appMessageService.send( + AppCustomizationSetStockAppTitleMessage(it.appType, it.name) + ) + } + } + }.launchIn(deviceScope) + } + + private fun sendConnectDisconnectEvents(deviceScope: CoroutineScope) { + deviceScope.launch { + try { + platformAppMessageIPC.broadcastPebbleConnected() + awaitCancellation() + } finally { + platformAppMessageIPC.broadcastPebbleDisconnected() + } + } + } + + private fun isAppActive(app: Uuid): Boolean { + val lastReceivedMessage = lastReceivedMessage + + return if (pebbleDevice.currentActiveApp.value == app) { + true + } else if (lastReceivedMessage != null && + lastReceivedMessage.app == app && + (Clock.System.now().toEpochMilliseconds() - lastReceivedMessage.timestamp + ) < 5.seconds.inWholeMilliseconds) { + // Sometimes app run state packets arrive with a delay. If we received incoming + // AppMessage from the app within last 5 seconds, consider it active + // and permit sending messages + true + } else { + Logging.w("Invalid AppMessage intent. " + + "Wanted to send a message to the app $app, but ${pebbleDevice.currentActiveApp.value} is active on the watch.", + ) + false + } + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt new file mode 100644 index 00000000..3dece1eb --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt @@ -0,0 +1,40 @@ +package io.rebble.cobble.shared.handlers + +import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.libpebblecommon.packets.AppRunStateMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class AppRunStateHandler( + private val pebbleDevice: PebbleDevice +) : CobbleHandler { + init { + pebbleDevice.negotiationScope.launch { + val deviceScope = pebbleDevice.connectionScope.filterNotNull().first() + deviceScope.launch { listenForAppStateChanges() }.invokeOnCompletion { + pebbleDevice.currentActiveApp.value = null + } + } + } + + private suspend fun listenForAppStateChanges() { + for (message in pebbleDevice.appRunStateService.receivedMessages) { + when (message) { + is AppRunStateMessage.AppRunStateStart -> { + pebbleDevice.currentActiveApp.value = message.uuid.get() + } + + is AppRunStateMessage.AppRunStateStop -> { + pebbleDevice.currentActiveApp.value = null + } + + else -> { + error("Unknown message type: $message") + } + } + } + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/CalendarActionHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/CalendarActionHandler.kt index 591d7ab3..5cfd6b09 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/CalendarActionHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/CalendarActionHandler.kt @@ -7,6 +7,7 @@ import io.rebble.cobble.shared.database.NextSyncAction import io.rebble.cobble.shared.database.dao.TimelinePinDao import io.rebble.cobble.shared.domain.calendar.CalendarAction import io.rebble.cobble.shared.domain.calendar.PlatformCalendarActionExecutor +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.domain.common.SystemAppIDs.calendarWatchappId import io.rebble.cobble.shared.domain.timeline.TimelineActionManager import io.rebble.cobble.shared.domain.timeline.WatchTimelineSyncer @@ -15,49 +16,52 @@ import io.rebble.libpebblecommon.packets.blobdb.TimelineItem import io.rebble.libpebblecommon.services.blobdb.TimelineService import io.rebble.libpebblecommon.util.TimelineAttributeFactory import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject -class CalendarActionHandler(private val scope: CoroutineScope): KoinComponent, CobbleHandler { +class CalendarActionHandler(private val pebbleDevice: PebbleDevice): KoinComponent, CobbleHandler { private val timelineActionManager: TimelineActionManager by inject() private val timelinePinDao: TimelinePinDao by inject() - private val timelineSyncer: WatchTimelineSyncer by inject() private val calendarActionExecutor: PlatformCalendarActionExecutor by inject() private val calendarActionFlow = timelineActionManager.actionFlowForApp(calendarWatchappId) + private val timelineSyncer = WatchTimelineSyncer(pebbleDevice.blobDBService) + init { - calendarActionFlow.onEach { - val (action, deferred) = it - val itemId = action.itemID.get() - val response = try { - when (val actionName = CalendarAction.fromID(action.actionID.get().toInt())) { - CalendarAction.Remove -> handleRemove(itemId) - CalendarAction.Mute -> handleMute(itemId) - CalendarAction.Accept, CalendarAction.Maybe, CalendarAction.Decline -> { - val pin = timelinePinDao.get(itemId) - if (pin != null) { - calendarActionExecutor.handlePlatformAction(actionName, pin) - } else { - Logging.w("Received calendar action for non-existent pin") - TimelineService.ActionResponse( - success = false - ) + pebbleDevice.negotiationScope.launch { + val deviceScope = pebbleDevice.connectionScope.filterNotNull().first() + calendarActionFlow.onEach { + val (action, deferred) = it + val itemId = action.itemID.get() + val response = try { + when (val actionName = CalendarAction.fromID(action.actionID.get().toInt())) { + CalendarAction.Remove -> handleRemove(itemId) + CalendarAction.Mute -> handleMute(itemId) + CalendarAction.Accept, CalendarAction.Maybe, CalendarAction.Decline -> { + val pin = timelinePinDao.get(itemId) + if (pin != null) { + calendarActionExecutor.handlePlatformAction(actionName, pin) + } else { + Logging.w("Received calendar action for non-existent pin") + TimelineService.ActionResponse( + success = false + ) + } } } + } catch (e: NoSuchElementException) { + TimelineService.ActionResponse( + success = false + ) } - } catch (e: NoSuchElementException) { - TimelineService.ActionResponse( - success = false - ) - } - deferred.complete(response) - }.catch { - Logging.e("Error while handling calendar action", it) - }.launchIn(scope) + deferred.complete(response) + }.catch { + Logging.e("Error while handling calendar action", it) + }.launchIn(deviceScope) + } } private suspend fun handleRemove(itemId: Uuid): TimelineService.ActionResponse { diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt new file mode 100644 index 00000000..7ff16b7c --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt @@ -0,0 +1,156 @@ +package io.rebble.cobble.shared.handlers + +import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.PlatformContext +import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.cobble.shared.domain.state.ConnectionState +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull +import io.rebble.libpebblecommon.PacketPriority +import io.rebble.libpebblecommon.packets.PhoneAppVersion +import io.rebble.libpebblecommon.packets.ProtocolCapsFlag +import io.rebble.libpebblecommon.packets.TimeMessage +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.offsetAt +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalUnsignedTypes::class) +class SystemHandler( + private val pebbleDevice: PebbleDevice, +) : CobbleHandler, KoinComponent { + private val negotiationScope = pebbleDevice.negotiationScope + private val systemService = pebbleDevice.systemService + private val platformContext: PlatformContext by inject() + + init { + pebbleDevice.systemService.appVersionRequestHandler = this::handleAppVersionRequest + listenForTimeChange() + + negotiationScope.launch { + // Wait until watch is connected before sending time + ConnectionStateManager.connectionState.first { it is ConnectionState.Connected } + + sendCurrentTime() + } + + negotiate() + } + + fun negotiationsComplete(watch: PebbleDevice) { + if (ConnectionStateManager.connectionState.value is ConnectionState.Negotiating) { + ConnectionStateManager.connectionState.value = ConnectionState.Connected(watch) + } else { + Logging.w("negotiationsComplete state mismatch!") + } + } + + fun recoveryMode(watch: PebbleDevice) { + if (ConnectionStateManager.connectionState.value is ConnectionState.Connected || ConnectionStateManager.connectionState.value is ConnectionState.Negotiating) { + ConnectionStateManager.connectionState.value = ConnectionState.RecoveryMode(watch) + } else { + Logging.w("recoveryMode state mismatch!") + } + } + + fun negotiate() { + negotiationScope.launch { + ConnectionStateManager.connectionState.first { it is ConnectionState.Negotiating } + Logging.i("Negotiating with watch") + refreshWatchMetadata() + ConnectionStateManager.connectedWatchMetadata.value?.let { + if (it.running.isRecovery.get()) { + Logging.i("Watch is in recovery mode, switching to recovery state") + ConnectionStateManager.connectionState.value.watchOrNull?.let { it1 -> recoveryMode(it1) } + } else { + ConnectionStateManager.connectionState.value.watchOrNull?.let { it1 -> negotiationsComplete(it1) } + } + } + } + } + + private suspend fun refreshWatchMetadata() { + var retries = 0 + while (retries < 3) { + try { + withTimeout(3000) { + val watchInfo = systemService.requestWatchVersion() + //FIXME: Possible race condition here + val watch = ConnectionStateManager.connectionState.value.watchOrNull + watch?.metadata?.value = watchInfo + val watchModel = systemService.requestWatchModel() + watch?.modelId?.value = watchModel + } + break + } catch (e: TimeoutCancellationException) { + Logging.e("Failed to get watch metadata, retrying", e) + retries++ + } catch (e: Exception) { + Logging.e("Failed to get watch metadata", e) + break + } + } + if (retries >= 3) { + Logging.e("Failed to get watch metadata after 3 retries, giving up and reconnecting") + //TODO: double check this works + negotiationScope.cancel("Failed to get watch metadata") + } + } + + + @OptIn(ExperimentalCoroutinesApi::class) + private fun listenForTimeChange() { + negotiationScope.launch { + val connectionScope = pebbleDevice.connectionScope.filterNotNull().first() + platformTimeChangedFlow(platformContext).onEach { + sendCurrentTime() + }.launchIn(connectionScope) + } + + } + + private suspend fun sendCurrentTime() { + val timezone = TimeZone.currentSystemDefault() + val now = Clock.System.now() + + val updateTimePacket = TimeMessage.SetUTC( + now.epochSeconds.toUInt(), + timezone.offsetAt(now).totalSeconds.seconds.inWholeHours.toShort(), + timezone.id + ) + + systemService.send(updateTimePacket, PacketPriority.LOW) + } + + private suspend fun handleAppVersionRequest(): PhoneAppVersion.AppVersionResponse { + val platformFlags = getPlatformPebbleFlags(platformContext) + + return PhoneAppVersion.AppVersionResponse( + UInt.MAX_VALUE, + 0u, + PhoneAppVersion.PlatformFlag.makeFlags( + platformContext.osType, + platformFlags.toList() + ), + 2u, + 4u, + 4u, + 2u, + ProtocolCapsFlag.makeFlags( + listOf( + ProtocolCapsFlag.Supports8kAppMessage, + ProtocolCapsFlag.SupportsExtendedMusicProtocol, + ProtocolCapsFlag.SupportsTwoWayDismissal, + ProtocolCapsFlag.SupportsAppRunStateProtocol + ) + ) + ) + } +} + +expect fun platformTimeChangedFlow(context: PlatformContext): Flow +expect fun getPlatformPebbleFlags(context: PlatformContext): Set \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt index 2f187164..4832ec86 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt @@ -6,5 +6,5 @@ import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent expect object JsRunnerFactory: KoinComponent { - fun createJsRunner(scope: CoroutineScope, connectedAddress: StateFlow, appInfo: PbwAppInfo, jsPath: String): JsRunner + fun createJsRunner(scope: CoroutineScope, connectedAddress: String, appInfo: PbwAppInfo, jsPath: String): JsRunner } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt new file mode 100644 index 00000000..b7057aa3 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt @@ -0,0 +1,45 @@ +package io.rebble.cobble.shared.js + +import com.benasher44.uuid.Uuid +import io.rebble.cobble.shared.PlatformContext +import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.handlers.getAppPbwFile +import io.rebble.cobble.shared.util.getPbwJsFilePath +import io.rebble.cobble.shared.util.requirePbwAppInfo +import io.rebble.cobble.shared.util.requirePbwJsFilePath +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout +import okio.buffer +import okio.use +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class PKJSApp(val uuid: Uuid): KoinComponent { + private val context: PlatformContext by inject() + private val pbw = getAppPbwFile(context, uuid.toString()) + private val appInfo = requirePbwAppInfo(pbw) + private val jsPath = requirePbwJsFilePath(context, appInfo, pbw) + private var jsRunner: JsRunner? = null + + companion object { + fun isJsApp(context: PlatformContext, uuid: Uuid): Boolean { + val pbw = getAppPbwFile(context, uuid.toString()) + return pbw.exists() && getPbwJsFilePath(context, requirePbwAppInfo(pbw), pbw) != null + } + } + + suspend fun start(device: PebbleDevice) { + withTimeout(1000) { + val connectionScope = device.connectionScope.filterNotNull().first() + jsRunner = JsRunnerFactory.createJsRunner(connectionScope, device.address, appInfo, jsPath) + } + jsRunner?.start() + } + + suspend fun stop() { + jsRunner?.stop() + jsRunner = null + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt index f4b15b7e..07051608 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt @@ -150,7 +150,7 @@ class PutBytesController: KoinComponent { } private fun launchNewPutBytesSession(block: suspend CoroutineScope.() -> Unit): Job { - return ConnectionStateManager.connectionScope.value.launch { + return ConnectionStateManager.connectionState.value.watchOrNull?.connectionScope?.value!!.launch { statusMutex.withLock { if (_status.value.state != State.IDLE) { throw IllegalStateException("Put bytes operation already in progress") diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt index 7b8444b6..f203767e 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt @@ -24,7 +24,7 @@ fun TestPage(onShowSnackbar: (String) -> Unit) { val koin = getKoin() Column { OutlinedButton(onClick = { - ConnectionStateManager.connectionScope.value.launch { + watchConnection.watchOrNull?.connectionScope?.value?.launch { val res = watchConnection.watchOrNull?.blobDBService?.send( BlobCommand.ClearCommand( token = Random.nextInt().toUShort(), diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.kt index 061d592f..7d8cc918 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.kt @@ -1,5 +1,7 @@ package io.rebble.cobble.shared.util +import com.benasher44.uuid.Uuid +import io.rebble.cobble.shared.PlatformContext import io.rebble.libpebblecommon.metadata.WatchType import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwManifest @@ -29,3 +31,12 @@ expect fun requirePbwAppInfo(pbwFile: File): PbwAppInfo * @throws IllegalStateException if pbw does not contain manifest with that watch type */ expect fun requirePbwBinaryBlob(pbwFile: File, watchType: WatchType, blobName: String): Source + +expect fun getPbwJsFilePath(context: PlatformContext, pbwAppInfo: PbwAppInfo, pbwFile: File): String? + +/** + * @throws IllegalStateException if pbw does not contain js file + */ +fun requirePbwJsFilePath(context: PlatformContext, pbwAppInfo: PbwAppInfo, pbwFile: File): String { + return getPbwJsFilePath(context, pbwAppInfo, pbwFile) ?: error("JS file not found in PBW") +} \ No newline at end of file diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/IOSPlatformContext.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/IOSPlatformContext.kt index 0dc00816..144a5b5e 100644 --- a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/IOSPlatformContext.kt +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/IOSPlatformContext.kt @@ -1,4 +1,7 @@ package io.rebble.cobble.shared -class IOSPlatformContext: PlatformContext { +import io.rebble.libpebblecommon.packets.PhoneAppVersion + +class IOSPlatformContext : PlatformContext { + override val osType: PhoneAppVersion.OSType = PhoneAppVersion.OSType.IOS } \ No newline at end of file diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.ios.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.ios.kt new file mode 100644 index 00000000..2e59da43 --- /dev/null +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.ios.kt @@ -0,0 +1,13 @@ +package io.rebble.cobble.shared.handlers + +import io.rebble.cobble.shared.PlatformContext +import io.rebble.libpebblecommon.packets.PhoneAppVersion +import kotlinx.coroutines.flow.Flow + +actual fun platformTimeChangedFlow(context: PlatformContext): Flow { + TODO("Not yet implemented") +} + +actual fun getPlatformPebbleFlags(context: PlatformContext): Set { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt index 238c3f76..303ffa60 100644 --- a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt @@ -6,5 +6,5 @@ import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent actual object JsRunnerFactory: KoinComponent { - actual fun createJsRunner(scope: CoroutineScope, connectedAddress: StateFlow, appInfo: PbwAppInfo, jsPath: String): JsRunner = TODO() + actual fun createJsRunner(scope: CoroutineScope, connectedAddress: String, appInfo: PbwAppInfo, jsPath: String): JsRunner = TODO() } \ No newline at end of file diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.ios.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.ios.kt index 73dcf077..e02a4fd2 100644 --- a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.ios.kt +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.ios.kt @@ -1,5 +1,7 @@ package io.rebble.cobble.shared.util +import com.benasher44.uuid.Uuid +import io.rebble.cobble.shared.PlatformContext import io.rebble.libpebblecommon.metadata.WatchType import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwManifest @@ -31,4 +33,8 @@ actual fun requirePbwAppInfo(pbwFile: File): PbwAppInfo { */ actual fun requirePbwBinaryBlob(pbwFile: File, watchType: WatchType, blobName: String): Source { TODO("Not yet implemented") +} + +actual fun getPbwJsFilePath(context: PlatformContext, pbwAppInfo: PbwAppInfo, pbwFile: File): String? { + TODO() } \ No newline at end of file From 00959910c6c4b1e850be9fa1117fa7b8c2f77c11 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 23 Sep 2024 14:55:22 +0100 Subject: [PATCH 09/20] =?UTF-8?q?refactor=20all=20the=20rest=20of=20the=20?= =?UTF-8?q?handlers=20+=20services=20into=20PebbleDevice=20=F0=9F=98=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/AndroidManifest.xml | 2 +- .../io/rebble/cobble/CobbleApplication.kt | 2 - .../cobble/bluetooth/ConnectionLooper.kt | 2 +- .../cobble/bluetooth/DeviceTransport.kt | 22 +-- .../bridges/common/AppInstallFlutterBridge.kt | 140 ------------------ .../bridges/common/AppLogFlutterBridge.kt | 48 +++--- .../bridges/common/ConnectionFlutterBridge.kt | 16 +- .../common/ScreenshotsFlutterBridge.kt | 15 +- .../cobble/bridges/ui/DebugFlutterBridge.kt | 10 +- .../ui/FirmwareUpdateControlFlutterBridge.kt | 27 ++-- .../bridges/ui/WorkaroundsFlutterBridge.kt | 2 +- .../io/rebble/cobble/di/AppComponent.kt | 26 +--- .../kotlin/io/rebble/cobble/di/AppModule.kt | 33 ++--- .../io/rebble/cobble/di/LibPebbleModule.kt | 135 ----------------- .../io/rebble/cobble/di/ServiceModule.kt | 61 -------- .../rebble/cobble/di/ServiceSubcomponent.kt | 26 ---- .../cobble/di/bridges/CommonBridgesModule.kt | 7 - .../io/rebble/cobble/log/LogSendingTask.kt | 12 +- .../io/rebble/cobble/service/InCallService.kt | 98 +++++++----- .../io/rebble/cobble/service/WatchService.kt | 19 --- .../java/io/rebble/cobble/bluetooth/BlueIO.kt | 3 +- .../cobble/bluetooth/EmulatedPebbleDevice.kt | 5 +- .../cobble/bluetooth/ble/BlueLEDriver.kt | 14 +- .../bluetooth/classic/BlueSerialDriver.kt | 52 +++---- .../bluetooth/classic/SocketSerialDriver.kt | 6 +- .../shared/datastore}/FlowablePreference.kt | 4 +- .../shared/datastore}/FlutterPreferences.kt | 18 +-- .../rebble/cobble/shared/di/AndroidModule.kt | 29 +++- .../shared/domain/PermissionChangeBus.kt | 2 +- .../CallNotificationProcessor.kt | 36 ++--- .../notifications}/NotificationGroup.kt | 2 +- .../notifications/NotificationListener.kt | 72 ++++----- .../notifications}/NotificationMessage.kt | 2 +- .../notifications/NotificationProcessor.kt | 37 ++--- .../cobble/shared/handlers/CalendarHandler.kt | 8 +- .../music/ActiveMediaSessionProvider.kt | 3 +- .../shared}/providers/PebbleKitProvider.kt | 28 ++-- .../UnboundWatchBeforeConnecting.kt | 4 +- .../workarounds/WorkaroundDescriptor.kt | 2 +- .../rebble/cobble/shared/di/CalendarModule.kt | 8 + .../rebble/cobble/shared/di/DatabaseModule.kt | 2 + .../cobble/shared/di/DependenciesModule.kt | 3 + .../cobble/shared/di/LibPebbleModule.kt | 45 ++++-- .../cobble/shared/di/PebbleDeviceModule.kt | 23 --- .../io/rebble/cobble/shared/di/StateModule.kt | 6 - .../shared/domain/common/PebbleDevice.kt | 47 +++++- .../shared/domain/state/ConnectionState.kt | 5 - .../domain/timeline/TimelineActionManager.kt | 5 +- .../shared}/errors/GlobalErrorHandler.kt | 9 +- .../shared/handlers/AppInstallHandler.kt | 8 +- .../cobble/shared/handlers/SystemHandler.kt | 22 +-- .../shared}/middleware/AppLogController.kt | 21 ++- .../shared}/middleware/DeviceLogController.kt | 17 +-- .../shared/middleware/PutBytesController.kt | 6 +- .../ui/viewmodel/LockerItemViewModel.kt | 17 ++- .../io/rebble/cobble/shared/di/IosModule.kt | 18 ++- 56 files changed, 459 insertions(+), 833 deletions(-) delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/di/LibPebbleModule.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/di/ServiceSubcomponent.kt rename android/{app/src/main/kotlin/io/rebble/cobble/datasources => shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore}/FlowablePreference.kt (89%) rename android/{app/src/main/kotlin/io/rebble/cobble/datasources => shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore}/FlutterPreferences.kt (84%) rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain}/notifications/CallNotificationProcessor.kt (89%) rename android/{app/src/main/kotlin/io/rebble/cobble/data => shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications}/NotificationGroup.kt (93%) rename android/{app/src/main/kotlin/io/rebble/cobble/data => shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications}/NotificationMessage.kt (73%) rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain}/notifications/NotificationProcessor.kt (93%) rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/androidMain/kotlin/io/rebble/cobble/shared}/providers/PebbleKitProvider.kt (84%) rename android/{pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth => shared/src/androidMain/kotlin/io/rebble/cobble/shared}/workarounds/UnboundWatchBeforeConnecting.kt (86%) rename android/{pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth => shared/src/androidMain/kotlin/io/rebble/cobble/shared}/workarounds/WorkaroundDescriptor.kt (85%) delete mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/PebbleDeviceModule.kt rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/commonMain/kotlin/io/rebble/cobble/shared}/errors/GlobalErrorHandler.kt (68%) rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/commonMain/kotlin/io/rebble/cobble/shared}/middleware/AppLogController.kt (60%) rename android/{app/src/main/kotlin/io/rebble/cobble => shared/src/commonMain/kotlin/io/rebble/cobble/shared}/middleware/DeviceLogController.kt (72%) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6f2304dc..a633ae76 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -262,7 +262,7 @@ diff --git a/android/app/src/main/kotlin/io/rebble/cobble/CobbleApplication.kt b/android/app/src/main/kotlin/io/rebble/cobble/CobbleApplication.kt index 5e3afbf9..1c630d2b 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/CobbleApplication.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/CobbleApplication.kt @@ -5,7 +5,6 @@ import io.rebble.cobble.di.AppComponent import io.rebble.cobble.di.DaggerAppComponent import io.rebble.cobble.log.AppTaggedDebugTree import io.rebble.cobble.log.FileLoggingTree -import io.rebble.cobble.shared.database.closeDatabase import io.rebble.cobble.shared.di.initKoin import timber.log.Timber import kotlin.system.exitProcess @@ -27,7 +26,6 @@ class CobbleApplication : FlutterApplication() { initKoin(applicationContext) component.initNotificationChannels() - component.initLibPebbleCommonServices() beginConnectingToDefaultWatch() } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index dde6a468..5de8355e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -97,7 +97,7 @@ class ConnectionLooper @Inject constructor( val connectionScope = CoroutineScope(SupervisorJob() + errorHandler + Dispatchers.IO) + CoroutineName("ConnectionScope-$macAddress") try { blueCommon.startSingleWatchConnection(macAddress).collect { - if (it is SingleConnectionStatus.Connected && connectionState.value !is ConnectionState.Connected && connectionState.value !is ConnectionState.RecoveryMode) { + if (it is SingleConnectionStatus.Connected /*&& connectionState.value !is ConnectionState.Connected && connectionState.value !is ConnectionState.RecoveryMode*/) { // initial connection, wait on negotiation _connectionState.value = ConnectionState.Negotiating(it.watch) } else { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index 7a02a866..11d68d1b 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -13,15 +13,14 @@ import io.rebble.cobble.bluetooth.classic.BlueSerialDriver import io.rebble.cobble.bluetooth.classic.SocketSerialDriver import io.rebble.cobble.bluetooth.scan.BleScanner import io.rebble.cobble.bluetooth.scan.ClassicScanner -import io.rebble.cobble.datasources.FlutterPreferences +import io.rebble.cobble.shared.datastore.FlutterPreferences import io.rebble.cobble.datasources.IncomingPacketsListener import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.ProtocolHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onCompletion +import org.koin.mp.KoinPlatformTools import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -31,7 +30,6 @@ class DeviceTransport @Inject constructor( private val context: Context, private val bleScanner: BleScanner, private val classicScanner: ClassicScanner, - private val protocolHandler: ProtocolHandler, private val flutterPreferences: FlutterPreferences, private val incomingPacketsListener: IncomingPacketsListener ) { @@ -59,15 +57,17 @@ class DeviceTransport @Inject constructor( lastMacAddress = macAddress val bluetoothDevice = if (BuildConfig.DEBUG && !macAddress.contains(":")) { - EmulatedPebbleDevice(macAddress, protocolHandler) + EmulatedPebbleDevice(macAddress) } else { val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() - BluetoothPebbleDevice(bluetoothAdapter.getRemoteDevice(macAddress), protocolHandler, macAddress) + BluetoothPebbleDevice(bluetoothAdapter.getRemoteDevice(macAddress), macAddress) } val driver = getTargetTransport(bluetoothDevice) this@DeviceTransport.driver = driver - return driver.startSingleWatchConnection(bluetoothDevice) + return driver.startSingleWatchConnection(bluetoothDevice).onCompletion { + bluetoothDevice.close() + } } @Throws(SecurityException::class) @@ -75,7 +75,7 @@ class DeviceTransport @Inject constructor( return when (pebbleDevice) { is EmulatedPebbleDevice -> { SocketSerialDriver( - protocolHandler, + pebbleDevice, incomingPacketsListener.receivedPackets ) } @@ -90,7 +90,7 @@ class DeviceTransport @Inject constructor( } BlueLEDriver( context = context, - protocolHandler = protocolHandler, + pebbleDevice = pebbleDevice, gattServerManager = gattServerManager, incomingPacketsListener = incomingPacketsListener.receivedPackets, ) { @@ -100,7 +100,7 @@ class DeviceTransport @Inject constructor( BluetoothDevice.DEVICE_TYPE_CLASSIC, BluetoothDevice.DEVICE_TYPE_DUAL -> { // Serial only device or serial/LE BlueSerialDriver( - protocolHandler, + pebbleDevice, incomingPacketsListener.receivedPackets ) } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt deleted file mode 100644 index 55b971cc..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt +++ /dev/null @@ -1,140 +0,0 @@ -package io.rebble.cobble.bridges.common - -import android.content.Context -import android.net.Uri -import androidx.core.net.toFile -import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.bridges.ui.BridgeLifecycleController -import io.rebble.cobble.data.pbw.appinfo.toPigeon -import io.rebble.cobble.shared.middleware.PutBytesController -import io.rebble.cobble.pigeons.NumberWrapper -import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.shared.util.findFile -import io.rebble.cobble.util.launchPigeonResult -import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo -import io.rebble.libpebblecommon.packets.AppOrderResultCode -import io.rebble.libpebblecommon.packets.AppReorderRequest -import io.rebble.libpebblecommon.packets.blobdb.BlobResponse -import io.rebble.libpebblecommon.services.AppReorderService -import kotlinx.coroutines.* -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromStream -import timber.log.Timber -import java.io.InputStream -import java.util.UUID -import java.util.zip.ZipInputStream -import javax.inject.Inject - -@Suppress("BlockingMethodInNonBlockingContext") -class AppInstallFlutterBridge @Inject constructor( - private val context: Context, - private val coroutineScope: CoroutineScope, - private val reorderService: AppReorderService, - private val putBytesController: PutBytesController, - bridgeLifecycleController: BridgeLifecycleController -) : FlutterBridge, Pigeons.AppInstallControl { - private val statusCallbacks = bridgeLifecycleController.createCallbacks( - Pigeons::AppInstallStatusCallbacks - ) - - private var statusObservingJob: Job? = null - - companion object { - private val json = Json { ignoreUnknownKeys = true } - } - - init { - bridgeLifecycleController.setupControl(Pigeons.AppInstallControl::setup, this) - } - - @Suppress("IfThenToElvis") - override fun getAppInfo( - arg: Pigeons.StringWrapper, - result: Pigeons.Result) { - coroutineScope.launchPigeonResult(result) { - val uri: String = arg.value!! - - - val parsingResult = parsePbwFileMetadata(uri) - if (parsingResult != null) { - parsingResult.toPigeon() - } else { - Pigeons.PbwAppInfo().apply { isValid = false; watchapp = Pigeons.WatchappInfo() } - } - } - } - - @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun parsePbwFileMetadata( - uri: String - ): PbwAppInfo? = withContext(Dispatchers.IO) { - try { - val stream = openUriStream(uri) - - ZipInputStream(stream).use { zipInputStream -> - zipInputStream.findFile("appinfo.json") ?: return@use null - parseAppInfoJson(zipInputStream) - } - } catch (e: Exception) { - Timber.e(e, "App parsing failed") - null - } - } - - override fun sendAppOrderToWatch( - arg: Pigeons.ListWrapper, - result: Pigeons.Result - ) { - coroutineScope.launchPigeonResult(result) { - val uuids = arg.value!!.map { UUID.fromString(it!! as String) } - reorderService.send( - AppReorderRequest(uuids) - ) - - val resultPacket = withTimeoutOrNull(10_000) { - reorderService.receivedMessages.receive() - } - - if (resultPacket?.status?.get() == AppOrderResultCode.SUCCESS.value) { - NumberWrapper(BlobResponse.BlobStatus.Success.value.toInt()) - } else { - NumberWrapper(BlobResponse.BlobStatus.WatchDisconnected.value.toInt()) - } - } - - } - - override fun subscribeToAppStatus() { - statusObservingJob = coroutineScope.launch { - putBytesController.status.collect { - val statusPigeon = Pigeons.AppInstallStatus.Builder() - .setIsInstalling(it.state == PutBytesController.State.SENDING) - .setProgress(it.progress) - .build() - - statusCallbacks.onStatusUpdated(statusPigeon) {} - } - } - } - - override fun unsubscribeFromAppStatus() { - statusObservingJob?.cancel() - } - - private fun openUriStream(uri: String): InputStream? { - val parsedUri = Uri.parse(uri) - - return when (parsedUri.scheme) { - "content" -> context.contentResolver.openInputStream(parsedUri) - "file" -> parsedUri.toFile().inputStream().buffered() - else -> { - Timber.e("Unknown uri type: %s", uri) - null - } - } - } - - private fun parseAppInfoJson(stream: InputStream): PbwAppInfo? { - return json.decodeFromStream(stream) - } -} diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppLogFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppLogFlutterBridge.kt index c96e0b74..da40f647 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppLogFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppLogFlutterBridge.kt @@ -2,18 +2,20 @@ package io.rebble.cobble.bridges.common import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController -import io.rebble.cobble.middleware.AppLogController +import io.rebble.cobble.shared.middleware.AppLogController import io.rebble.cobble.pigeons.Pigeons +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject class AppLogFlutterBridge @Inject constructor( - bridgeLifecycleController: BridgeLifecycleController, - private val coroutineScope: CoroutineScope, - private val appLogController: AppLogController + bridgeLifecycleController: BridgeLifecycleController ) : FlutterBridge, Pigeons.AppLogControl { private val callbacks = bridgeLifecycleController.createCallbacks(Pigeons::AppLogCallbacks) @@ -25,21 +27,29 @@ class AppLogFlutterBridge @Inject constructor( override fun startSendingLogs() { stopSendingLogs() - - logsJob = coroutineScope.launch { - appLogController.logs.collect { - Timber.d("Received in pigeon '%s'", it.message.get()) - callbacks.onLogReceived( - Pigeons.AppLogEntry.Builder() - .setUuid(it.uuid.get().toString()) - .setTimestamp(it.timestamp.get().toLong()) - .setLevel(it.level.get().toLong()) - .setLineNumber(it.lineNumber.get().toLong()) - .setFilename(it.filename.get()) - .setMessage(it.message.get()) - .build() - - ) {} + val pebbleDevice = ConnectionStateManager.connectionState.value.watchOrNull + ?: run { + Timber.e("No app log service available") + return + } + pebbleDevice.negotiationScope.launch { + val connectionScope = pebbleDevice.connectionScope.filterNotNull().first() + logsJob = connectionScope.launch { + val appLogController = AppLogController(pebbleDevice) + appLogController.logs.collect { + Timber.d("Received in pigeon '%s'", it.message.get()) + callbacks.onLogReceived( + Pigeons.AppLogEntry.Builder() + .setUuid(it.uuid.get().toString()) + .setTimestamp(it.timestamp.get().toLong()) + .setLevel(it.level.get().toLong()) + .setLineNumber(it.lineNumber.get().toLong()) + .setFilename(it.filename.get()) + .setMessage(it.message.get()) + .build() + + ) {} + } } } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt index 4f6115f5..1f3744a4 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt @@ -11,13 +11,14 @@ import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.* import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import javax.inject.Inject class ConnectionFlutterBridge @Inject constructor( bridgeLifecycleController: BridgeLifecycleController, private val connectionLooper: ConnectionLooper, - private val coroutineScope: CoroutineScope, - private val protocolHandler: ProtocolHandler + private val coroutineScope: CoroutineScope ) : FlutterBridge, Pigeons.ConnectionControl { private val connectionCallbacks = bridgeLifecycleController .createCallbacks(Pigeons::ConnectionCallbacks) @@ -41,19 +42,14 @@ class ConnectionFlutterBridge @Inject constructor( @Suppress("UNCHECKED_CAST") override fun sendRawPacket(arg: Pigeons.ListWrapper) { - coroutineScope.launch { - val byteArray = (arg.value as List).map { it.toByte().toUByte() }.toUByteArray() - protocolHandler.send(byteArray) - } + error("Deprecated") } override fun observeConnectionChanges() { statusObservingJob = coroutineScope.launch(Dispatchers.Main) { - combine( - connectionLooper.connectionState, - ConnectionStateManager.connectedWatchMetadata, - ) { connectionState, watchMetadata -> + ConnectionStateManager.connectionState.map { connectionState -> val bluetoothDevice = connectionState.watchOrNull + val watchMetadata = connectionState.watchOrNull?.metadata?.value val model = watchMetadata?.running?.hardwarePlatform?.get()?.toInt() Pigeons.WatchConnectionStatePigeon.Builder() .setIsConnected(connectionState is ConnectionState.Connected || diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScreenshotsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScreenshotsFlutterBridge.kt index 97f525e8..64f70b77 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScreenshotsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScreenshotsFlutterBridge.kt @@ -6,6 +6,8 @@ import android.graphics.Color import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.pigeons.Pigeons +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.cobble.util.launchPigeonResult import io.rebble.libpebblecommon.packets.* import io.rebble.libpebblecommon.services.ScreenshotService @@ -24,8 +26,7 @@ import kotlin.experimental.and class ScreenshotsFlutterBridge @Inject constructor( private val context: Context, bridgeLifecycleController: BridgeLifecycleController, - private val coroutineScope: CoroutineScope, - private val screenshotService: ScreenshotService + private val coroutineScope: CoroutineScope ) : FlutterBridge, Pigeons.ScreenshotsControl { init { bridgeLifecycleController.setupControl(Pigeons.ScreenshotsControl::setup, this) @@ -35,9 +36,9 @@ class ScreenshotsFlutterBridge @Inject constructor( coroutineScope.launchPigeonResult(result) { try { - screenshotService.send(ScreenshotRequest()) + ConnectionStateManager.connectionState.value.watchOrNull?.screenshotService?.send(ScreenshotRequest()) ?: return@launchPigeonResult Pigeons.ScreenshotResult.Builder().setSuccess(false).build() - val firstResult = receiveScreenshotResponse() + val firstResult = receiveScreenshotResponse() ?: return@launchPigeonResult Pigeons.ScreenshotResult.Builder().setSuccess(false).build() val header = ScreenshotHeader().apply { m.fromBytes(DataBuffer(firstResult.data.get())) @@ -73,7 +74,7 @@ class ScreenshotsFlutterBridge @Inject constructor( buffer.write(header.data.get().asByteArray()) while (buffer.size < expectedBytes) { - val nextSegment = receiveScreenshotResponse() + val nextSegment = receiveScreenshotResponse() ?: return@launchPigeonResult Pigeons.ScreenshotResult.Builder().setSuccess(false).build() buffer.write(nextSegment.data.get().asByteArray()) } @@ -110,9 +111,9 @@ class ScreenshotsFlutterBridge @Inject constructor( } } - private suspend fun receiveScreenshotResponse(): ScreenshotResponse { + private suspend fun receiveScreenshotResponse(): ScreenshotResponse? { return withTimeout(10_000) { - screenshotService.receivedMessages.receive() + ConnectionStateManager.connectionState.value.watchOrNull?.screenshotService?.receivedMessages?.receive() } } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt index 96cc5219..d99bf019 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt @@ -2,7 +2,7 @@ package io.rebble.cobble.bridges.ui import android.content.Context import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.errors.GlobalExceptionHandler +import io.rebble.cobble.shared.errors.GlobalExceptionHandler import io.rebble.cobble.log.collectAndShareLogs import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.shared.datastore.KMPPrefs @@ -14,10 +14,10 @@ import kotlinx.coroutines.launch import javax.inject.Inject class DebugFlutterBridge @Inject constructor( - private val context: Context, - private val prefs: KMPPrefs, - private val globalExceptionHandler: GlobalExceptionHandler, - bridgeLifecycleController: BridgeLifecycleController + private val context: Context, + private val prefs: KMPPrefs, + private val globalExceptionHandler: GlobalExceptionHandler, + bridgeLifecycleController: BridgeLifecycleController ) : FlutterBridge, Pigeons.DebugControl { private val scope = CoroutineScope(Dispatchers.Main + globalExceptionHandler) init { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt index 0587c27f..d9eff2e8 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt @@ -8,6 +8,7 @@ import io.rebble.cobble.shared.middleware.PutBytesController import io.rebble.cobble.pigeons.BooleanWrapper import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.cobble.util.launchPigeonResult import io.rebble.cobble.shared.util.zippedSource import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform @@ -32,8 +33,6 @@ import javax.inject.Inject class FirmwareUpdateControlFlutterBridge @Inject constructor( bridgeLifecycleController: BridgeLifecycleController, private val coroutineScope: CoroutineScope, - private val systemService: SystemService, - private val putBytesController: PutBytesController, private val context: Context ) : FlutterBridge, Pigeons.FirmwareUpdateControl { init { @@ -62,7 +61,7 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( } val hardwarePlatformNumber = withTimeoutOrNull(2_000) { - ConnectionStateManager.connectedWatchMetadata.first { it != null } + ConnectionStateManager.connectionState.first { it.watchOrNull?.metadata?.value != null }.watchOrNull?.metadata?.value } ?.running ?.hardwarePlatform @@ -91,7 +90,7 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( timezone.id ) - systemService.send(updateTimePacket) + ConnectionStateManager.connectionState.value.watchOrNull?.systemService?.send(updateTimePacket) } override fun beginFirmwareUpdate(fwUri: Pigeons.StringWrapper, result: Pigeons.Result) { @@ -128,7 +127,7 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( } val lastConnectedWatch = withTimeoutOrNull(2_000) { - ConnectionStateManager.connectedWatchMetadata.first { it != null } + ConnectionStateManager.connectionState.first { it.watchOrNull?.metadata?.value != null }.watchOrNull?.metadata?.value } ?: error("Watch not connected") @@ -147,27 +146,29 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( Timber.i("All checks passed, starting firmware update") sendTime() - val response = systemService.firmwareUpdateStart(0u, (manifest.firmware.size + (manifest.resources?.size + val updatingDevice = ConnectionStateManager.connectionState.value.watchOrNull + val connectionScope = updatingDevice?.connectionScope?.value ?: error("Watch not connected") + val response = updatingDevice.systemService.firmwareUpdateStart(0u, (manifest.firmware.size + (manifest.resources?.size ?: 0)).toUInt()) Timber.d("Firmware update start response: $response") firmwareUpdateCallbacks.onFirmwareUpdateStarted {} - val job = coroutineScope.launch { + val job = connectionScope.launch { try { - putBytesController.status.collect { + updatingDevice.putBytesController.status.collect { firmwareUpdateCallbacks.onFirmwareUpdateProgress(it.progress) {} } } catch (_: CancellationException) { } } try { - putBytesController.startFirmwareInstall(firmwareBin, systemResources, manifest).join() + updatingDevice.putBytesController.startFirmwareInstall(firmwareBin, systemResources, manifest).join() } finally { job.cancel() - if (putBytesController.lastProgress != 1.0) { - systemService.send(SystemMessage.FirmwareUpdateFailed()) - error("Firmware update failed - Only reached ${putBytesController.status.value.progress}") + if (updatingDevice.putBytesController.lastProgress != 1.0) { + updatingDevice.systemService.send(SystemMessage.FirmwareUpdateFailed()) + error("Firmware update failed - Only reached ${updatingDevice.putBytesController.status.value.progress}") } else { - systemService.send(SystemMessage.FirmwareUpdateComplete()) + updatingDevice.systemService.send(SystemMessage.FirmwareUpdateComplete()) firmwareUpdateCallbacks.onFirmwareUpdateFinished {} } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/WorkaroundsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/WorkaroundsFlutterBridge.kt index aad5e41b..48fc6c76 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/WorkaroundsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/WorkaroundsFlutterBridge.kt @@ -1,7 +1,7 @@ package io.rebble.cobble.bridges.ui import android.content.Context -import io.rebble.cobble.bluetooth.workarounds.WorkaroundDescriptor +import io.rebble.cobble.shared.workarounds.WorkaroundDescriptor import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.pigeons.ListWrapper import io.rebble.cobble.pigeons.Pigeons diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt index 26b64a8e..e9ae14a6 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt @@ -8,52 +8,32 @@ import dagger.Component import io.rebble.cobble.NotificationChannelManager import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bluetooth.DeviceTransport -import io.rebble.cobble.datasources.FlutterPreferences import io.rebble.cobble.datasources.PairedStorage -import io.rebble.cobble.errors.GlobalExceptionHandler -import io.rebble.cobble.middleware.AppLogController -import io.rebble.cobble.middleware.DeviceLogController -import io.rebble.cobble.notifications.CallNotificationProcessor -import io.rebble.cobble.notifications.NotificationProcessor +import io.rebble.cobble.shared.middleware.DeviceLogController 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.errors.GlobalExceptionHandler import io.rebble.cobble.shared.jobs.AndroidJobScheduler -import io.rebble.libpebblecommon.ProtocolHandler -import io.rebble.libpebblecommon.services.PhoneControlService -import io.rebble.libpebblecommon.services.ProtocolService -import io.rebble.libpebblecommon.services.blobdb.BlobDBService -import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Singleton @Singleton @Component(modules = [ - AppModule::class, - LibPebbleModule::class + AppModule::class ]) interface AppComponent { - fun createPhoneControlService(): PhoneControlService fun createBlueCommon(): DeviceTransport - fun createProtocolHandler(): ProtocolHandler fun createExceptionHandler(): GlobalExceptionHandler fun createConnectionLooper(): ConnectionLooper fun createPairedStorage(): PairedStorage - fun createNotificationProcessor(): NotificationProcessor - fun createCallNotificationProcessor(): CallNotificationProcessor - fun createFlutterPreferences(): FlutterPreferences - fun createAppLogController(): AppLogController fun initServiceLifecycleControl(): ServiceLifecycleControl fun initNotificationChannels(): NotificationChannelManager - fun createDeviceLogController(): DeviceLogController - - fun initLibPebbleCommonServices(): Set fun createActivitySubcomponentFactory(): ActivitySubcomponent.Factory fun createFlutterActivitySubcomponentFactory(): FlutterActivitySubcomponent.Factory - fun createServiceSubcomponentFactory(): ServiceSubcomponent.Factory //TODO: Unify DI under Koin fun createKMPCalendarSync(): CalendarSync diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt index 046a0913..91b6b22c 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt @@ -7,28 +7,26 @@ import android.service.notification.StatusBarNotification import dagger.Binds import dagger.Module import dagger.Provides -import io.rebble.cobble.errors.GlobalExceptionHandler -import io.rebble.cobble.middleware.DeviceLogController +import io.rebble.cobble.shared.errors.GlobalExceptionHandler +import io.rebble.cobble.shared.middleware.DeviceLogController import io.rebble.cobble.shared.database.AppDatabase import io.rebble.cobble.shared.database.dao.CachedPackageInfoDao import io.rebble.cobble.shared.database.dao.NotificationChannelDao import io.rebble.cobble.shared.database.dao.PersistedNotificationDao +import io.rebble.cobble.shared.datastore.FlutterPreferences import io.rebble.cobble.shared.datastore.KMPPrefs import io.rebble.cobble.shared.domain.calendar.CalendarSync -import io.rebble.cobble.shared.domain.common.PebbleDevice -import io.rebble.cobble.shared.domain.state.ConnectionState 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.cobble.shared.middleware.PutBytesController import io.rebble.libpebblecommon.services.LogDumpService -import io.rebble.libpebblecommon.services.blobdb.BlobDBService import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.plus +import org.koin.core.parameter.parametersOf import org.koin.core.qualifier.named import org.koin.mp.KoinPlatformTools import java.util.UUID @@ -56,17 +54,14 @@ abstract class AppModule { return CalendarSync(CoroutineScope(Dispatchers.Default) + exceptionHandler) } @Provides - @Singleton - fun provideCalendarActionHandler( - exceptionHandler: CoroutineExceptionHandler - ): CalendarActionHandler { - return CalendarActionHandler(CoroutineScope(Dispatchers.Default) + exceptionHandler) - } - @Provides fun provideKMPPrefs(context: Context): KMPPrefs { return KMPPrefs() } @Provides + fun provideGlobalExceptionHandler(): GlobalExceptionHandler { + return KoinPlatformTools.defaultContext().get().get() + } + @Provides fun provideTokenState(): MutableStateFlow { return KoinPlatformTools.defaultContext().get().get(named("currentToken")) } @@ -89,12 +84,6 @@ abstract class AppModule { return KoinPlatformTools.defaultContext().get().get() } - @Provides - @Singleton - fun providePutBytesController(): PutBytesController { - return KoinPlatformTools.defaultContext().get().get() - } - @Provides @Singleton fun provideActiveNotifsState(): MutableStateFlow> { @@ -103,10 +92,8 @@ abstract class AppModule { @Provides @Singleton - fun provideDeviceLogController( - logDumpService: LogDumpService, - ): DeviceLogController { - return DeviceLogController(logDumpService) + fun provideFlutterPreferences(): FlutterPreferences { + return KoinPlatformTools.defaultContext().get().get() } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/LibPebbleModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/LibPebbleModule.kt deleted file mode 100644 index bcecfcaa..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/LibPebbleModule.kt +++ /dev/null @@ -1,135 +0,0 @@ -package io.rebble.cobble.di - -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.multibindings.IntoSet -import io.rebble.cobble.shared.database.AppDatabase -import io.rebble.libpebblecommon.ProtocolHandler -import io.rebble.libpebblecommon.ProtocolHandlerImpl -import io.rebble.libpebblecommon.services.* -import io.rebble.libpebblecommon.services.app.AppRunStateService -import io.rebble.libpebblecommon.services.appmessage.AppMessageService -import io.rebble.libpebblecommon.services.blobdb.BlobDBService -import io.rebble.libpebblecommon.services.blobdb.TimelineService -import io.rebble.libpebblecommon.services.notification.NotificationService -import org.koin.mp.KoinPlatformTools -import javax.inject.Singleton - -@Module -abstract class LibPebbleModule { - @Module - companion object { - @Provides - @Singleton - fun provideProtocolHandler(): ProtocolHandler = KoinPlatformTools.defaultContext().get().get() - - @Provides - @Singleton - fun providePhoneControlService( - protocolHandler: ProtocolHandler - ) = PhoneControlService(protocolHandler) - - @Provides - @Singleton - fun provideAppMessageService( - protocolHandler: ProtocolHandler - ) = AppMessageService(protocolHandler) - - @Provides - @Singleton - fun provideSystemService( - protocolHandler: ProtocolHandler - ) = SystemService(protocolHandler) - - @Provides - @Singleton - fun provideTimelineService() = KoinPlatformTools.defaultContext().get().get() - - @Provides - @Singleton - fun provideMusicService( - protocolHandler: ProtocolHandler - ) = MusicService(protocolHandler) - - @Provides - @Singleton - fun provideAppFetchService( - protocolHandler: ProtocolHandler - ) = KoinPlatformTools.defaultContext().get().get() - - @Provides - @Singleton - fun providePutBytesService( - protocolHandler: ProtocolHandler - ) = KoinPlatformTools.defaultContext().get().get() - - @Provides - @Singleton - fun provideAppReorderService( - protocolHandler: ProtocolHandler - ) = AppReorderService(protocolHandler) - - @Provides - @Singleton - fun provideScreenshotService( - protocolHandler: ProtocolHandler - ) = ScreenshotService(protocolHandler) - - @Provides - @Singleton - fun provideAppLogService( - protocolHandler: ProtocolHandler - ) = AppLogService(protocolHandler) - - @Provides - @Singleton - fun provideLogDumpService( - protocolHandler: ProtocolHandler - ) = LogDumpService(protocolHandler) - } - - @Binds - @IntoSet - abstract fun bindPhoneControlServiceIntoSet(phoneControlService: PhoneControlService): ProtocolService - - @Binds - @IntoSet - abstract fun bindAppMessageServiceIntoSet(appMessageService: AppMessageService): ProtocolService - - @Binds - @IntoSet - abstract fun bindSystemServiceIntoSet(systemService: SystemService): ProtocolService - - @Binds - @IntoSet - abstract fun bindTimelineServiceIntoSet(timelineService: TimelineService): ProtocolService - - @Binds - @IntoSet - abstract fun bindMusicServiceIntoSet(musicService: MusicService): ProtocolService - - @Binds - @IntoSet - abstract fun bindAppFetchServiceIntoSet(service: AppFetchService): ProtocolService - - @Binds - @IntoSet - abstract fun bindPutBytesServiceIntoSet(service: PutBytesService): ProtocolService - - @Binds - @IntoSet - abstract fun bindAppReorderServiceIntoSet(service: AppReorderService): ProtocolService - - @Binds - @IntoSet - abstract fun bindScrenshotServiceIntoSet(service: ScreenshotService): ProtocolService - - @Binds - @IntoSet - abstract fun bindAppLogServiceIntoSet(service: AppLogService): ProtocolService - - @Binds - @IntoSet - abstract fun bindLogDumpServiceIntoSet(service: LogDumpService): ProtocolService -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt deleted file mode 100644 index d2a9f734..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt +++ /dev/null @@ -1,61 +0,0 @@ -package io.rebble.cobble.di - -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.multibindings.IntoSet -import io.rebble.cobble.shared.handlers.music.MusicHandler -import io.rebble.cobble.shared.domain.notifications.NotificationActionHandler -import io.rebble.cobble.shared.handlers.AppInstallHandler -import io.rebble.cobble.shared.handlers.CalendarHandler -import io.rebble.cobble.shared.handlers.CobbleHandler -import kotlinx.coroutines.CoroutineScope -import javax.inject.Named - -@Module -abstract class ServiceModule { - @Module - companion object { - @Provides - fun provideNotificationActionHandler(scope: CoroutineScope): NotificationActionHandler { - return NotificationActionHandler(scope) - } - } - - //TODO: Move to per-protocol handler services - /* - @Binds - @IntoSet - @Named("normal") - abstract fun bindAppMessageHandlerIntoSet( - appMessageHandler: AppMessageHandler - ): CobbleHandler - */ - - @Binds - @IntoSet - @Named("normal") - abstract fun bindCalendarHandlerIntoSet( - calendarHandler: CalendarHandler - ): CobbleHandler - @Binds - @IntoSet - @Named("normal") - abstract fun bindNotificationActionHandlerIntoSet( - notificationHandler: NotificationActionHandler - ): CobbleHandler - - @Binds - @IntoSet - @Named("normal") - abstract fun bindMusicHandlerIntoSet( - musicHandler: MusicHandler - ): CobbleHandler - - @Binds - @IntoSet - @Named("normal") - abstract fun bindAppInstallHandlerIntoSet( - appInstallHandler: AppInstallHandler - ): CobbleHandler -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceSubcomponent.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceSubcomponent.kt deleted file mode 100644 index 86465465..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceSubcomponent.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.rebble.cobble.di - -import dagger.BindsInstance -import dagger.Subcomponent -import io.rebble.cobble.shared.handlers.CobbleHandler -import io.rebble.cobble.service.WatchService -import javax.inject.Named -import javax.inject.Provider - -@Subcomponent( - modules = [ - ServiceModule::class - ] -) -interface ServiceSubcomponent { - @Named("negotiation") - fun getNegotiationMessageHandlersProvider(): Provider> - - @Named("normal") - fun getNormalMessageHandlersProvider(): Provider> - - @Subcomponent.Factory - interface Factory { - fun create(@BindsInstance watchService: WatchService): ServiceSubcomponent - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/bridges/CommonBridgesModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/bridges/CommonBridgesModule.kt index e1a0c679..c2ec2beb 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/bridges/CommonBridgesModule.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/bridges/CommonBridgesModule.kt @@ -54,13 +54,6 @@ abstract class CommonBridgesModule { packageDetailsFlutterBridge: PackageDetailsFlutterBridge ): FlutterBridge - @Binds - @IntoSet - @CommonBridge - abstract fun bindAppInstallBridge( - appInstallFlutterBridge: AppInstallFlutterBridge - ): FlutterBridge - @Binds @IntoSet @CommonBridge diff --git a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt index 7605083e..a57e9087 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt @@ -9,7 +9,9 @@ import android.os.Build import androidx.core.content.FileProvider import io.rebble.cobble.BuildConfig import io.rebble.cobble.CobbleApplication -import io.rebble.cobble.middleware.DeviceLogController +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull +import io.rebble.cobble.shared.middleware.DeviceLogController import io.rebble.cobble.shared.util.hasNotificationAccessPermission import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import io.rebble.libpebblecommon.packets.LogDump @@ -41,10 +43,9 @@ private fun generateDebugInfo(context: Context, rwsId: String): String { val inj = (context.applicationContext as CobbleApplication).component val connectionLooper = inj.createConnectionLooper() - val watchMetadataStore = inj.createWatchMetadataStore() val connectionState = connectionLooper.connectionState.value - val watchMeta = watchMetadataStore.lastConnectedWatchMetadata.value + val watchMeta = ConnectionStateManager.connectionState.value.watchOrNull?.metadata?.value val watchModel = watchMeta?.running?.hardwarePlatform?.get()?.let { WatchHardwarePlatform.fromProtocolNumber(it) } @@ -106,8 +107,9 @@ fun collectAndShareLogs(context: Context, rwsId: String) = GlobalScope.launch(Di var zipOutputStream: ZipOutputStream? = null val debugInfo = generateDebugInfo(context, rwsId) - val deviceLogController = (context.applicationContext as CobbleApplication).component.createDeviceLogController() - val deviceLogs = getDeviceLogs(deviceLogController) + val device = ConnectionStateManager.connectionState.value.watchOrNull + val deviceLogController = device?.let {DeviceLogController(it)} + val deviceLogs = deviceLogController?.let {getDeviceLogs(it)} try { zipOutputStream = ZipOutputStream(FileOutputStream(targetFile)) for (file in logsFolder.listFiles() ?: emptyArray()) { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt index 4a5e0d34..95cd6bc8 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt @@ -9,20 +9,20 @@ import android.telecom.InCallService import android.telecom.VideoProfile import io.rebble.cobble.CobbleApplication import io.rebble.cobble.bluetooth.ConnectionLooper -import io.rebble.cobble.notifications.CallNotificationProcessor +import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.domain.notifications.CallNotificationProcessor import io.rebble.cobble.shared.domain.state.ConnectionState +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.libpebblecommon.packets.PhoneControl import io.rebble.libpebblecommon.services.PhoneControlService import kotlinx.coroutines.* -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.* import timber.log.Timber import kotlin.random.Random class InCallService : InCallService() { private lateinit var coroutineScope: CoroutineScope - private lateinit var phoneControlService: PhoneControlService private lateinit var connectionLooper: ConnectionLooper private lateinit var contentResolver: ContentResolver private lateinit var callNotificationProcessor: CallNotificationProcessor @@ -34,57 +34,65 @@ class InCallService : InCallService() { super.onCreate() Timber.d("InCallService created") val injectionComponent = (applicationContext as CobbleApplication).component - phoneControlService = injectionComponent.createPhoneControlService() connectionLooper = injectionComponent.createConnectionLooper() coroutineScope = CoroutineScope( SupervisorJob() + injectionComponent.createExceptionHandler() ) - callNotificationProcessor = injectionComponent.createCallNotificationProcessor() contentResolver = applicationContext.contentResolver listenForPhoneControlMessages() } private fun listenForPhoneControlMessages() { - phoneControlService.receivedMessages.receiveAsFlow().onEach { - if (connectionLooper.connectionState.value !is ConnectionState.Connected) { - Timber.w("Ignoring phone control message because watch is not connected") - return@onEach - } - when (it) { - is PhoneControl.Answer -> { - synchronized(this@InCallService) { - if (it.cookie.get() == lastCookie) { - lastCall?.answer(VideoProfile.STATE_AUDIO_ONLY) // Answering from watch probably means a headset or something - } else { - callNotificationProcessor.handleCallAction(it) + ConnectionStateManager.connectionState.filterIsInstance().onEach { + val pebbleDevice = it.watch + val connectionScope = pebbleDevice.connectionScope.filterNotNull().first() + val phoneControlService = pebbleDevice.phoneControlService + phoneControlService.receivedMessages.receiveAsFlow().onEach { + if (connectionLooper.connectionState.value !is ConnectionState.Connected) { + Timber.w("Ignoring phone control message because watch is not connected") + return@onEach + } + when (it) { + is PhoneControl.Answer -> { + synchronized(this@InCallService) { + if (it.cookie.get() == lastCookie) { + lastCall?.answer(VideoProfile.STATE_AUDIO_ONLY) // Answering from watch probably means a headset or something + } else { + callNotificationProcessor.handleCallAction(it) + } } } - } - is PhoneControl.Hangup -> { - synchronized(this@InCallService) { - if (it.cookie.get() == lastCookie) { - lastCookie = null - lastCall?.let { call -> - if (call.details.state == Call.STATE_RINGING) { - Timber.d("Rejecting ringing call") - call.reject(Call.REJECT_REASON_DECLINED) - } else { - Timber.d("Disconnecting call") - call.disconnect() + is PhoneControl.Hangup -> { + synchronized(this@InCallService) { + if (it.cookie.get() == lastCookie) { + lastCookie = null + lastCall?.let { call -> + if (call.state == Call.STATE_RINGING) { + Timber.d("Rejecting ringing call") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + call.reject(Call.REJECT_REASON_DECLINED) + } else { + call.reject(false, null) + } + } else { + Timber.d("Disconnecting call") + call.disconnect() + } } + } else { + callNotificationProcessor.handleCallAction(it) } - } else { - callNotificationProcessor.handleCallAction(it) } } - } - else -> { - Timber.w("Unhandled phone control message: $it") + else -> { + Timber.w("Unhandled phone control message: $it") + } } - } - }.launchIn(coroutineScope) + }.launchIn(connectionScope) + } + } override fun onDestroy() { @@ -97,12 +105,17 @@ class InCallService : InCallService() { super.onCallAdded(call) Timber.d("Call added") coroutineScope.launch(Dispatchers.IO) { + val pebbleDevice = ConnectionStateManager.connectionState.value.watchOrNull + val phoneControlService = pebbleDevice?.phoneControlService ?: run { + Logging.w("Phone control service not available (e.g. device disconnected)") + return@launch + } synchronized(this@InCallService) { if (lastCookie != null) { lastCookie = if (lastCall == null) { null } else { - if (lastCall?.details?.state == Call.STATE_DISCONNECTED) { + if (lastCall?.state == Call.STATE_DISCONNECTED) { null } else { Timber.w("Ignoring call because there is already a call in progress") @@ -116,7 +129,7 @@ class InCallService : InCallService() { synchronized(this@InCallService) { lastCookie = cookie } - if (call.details.state == Call.STATE_RINGING) { + if (call.state == Call.STATE_RINGING) { coroutineScope.launch(Dispatchers.IO) { phoneControlService.send( PhoneControl.IncomingCall( @@ -204,6 +217,11 @@ class InCallService : InCallService() { super.onCallRemoved(call) Timber.d("Call removed") coroutineScope.launch(Dispatchers.IO) { + val pebbleDevice = ConnectionStateManager.connectionState.value.watchOrNull + val phoneControlService = pebbleDevice?.phoneControlService ?: run { + Logging.w("Phone control service not available (e.g. device disconnected)") + return@launch + } val cookie = synchronized(this@InCallService) { val c = lastCookie ?: return@launch lastCookie = null diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt index 5370a747..9801b1a3 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt @@ -45,11 +45,8 @@ class WatchService : LifecycleService() { startForeground(NotificationId.WATCH_CONNECTION, mainNotifBuilder.build()) val injectionComponent = (applicationContext as CobbleApplication).component - val serviceComponent = injectionComponent.createServiceSubcomponentFactory() - .create(this) coroutineScope = lifecycleScope + injectionComponent.createExceptionHandler() - protocolHandler = injectionComponent.createProtocolHandler() connectionLooper = injectionComponent.createConnectionLooper() calendarSync = injectionComponent.createKMPCalendarSync() @@ -61,7 +58,6 @@ class WatchService : LifecycleService() { } startNotificationLoop() - startHandlersLoop(serviceComponent.getNegotiationMessageHandlersProvider(), serviceComponent.getNormalMessageHandlersProvider()) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -143,19 +139,4 @@ class WatchService : LifecycleService() { .Builder(this@WatchService, channel) .setContentIntent(mainActivityIntent) } - - private fun startHandlersLoop(negotiationHandlers: Provider>, normalHandlers: Provider>) { - coroutineScope.launch { - connectionLooper.connectionState - .filter { it is ConnectionState.Connected || it is ConnectionState.Negotiating } - .collect { - watchConnectionScope = connectionLooper - .getWatchConnectedScope(Dispatchers.Main.immediate) - negotiationHandlers.get() - if (it is ConnectionState.Connected) { - normalHandlers.get() - } - } - } - } } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt index 8ab14abb..25fe2b9b 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt @@ -18,9 +18,8 @@ interface BlueIO { class BluetoothPebbleDevice( val bluetoothDevice: BluetoothDevice, - protocolHandler: ProtocolHandler, address: String -) : PebbleDevice(null, protocolHandler, address){ +) : PebbleDevice(null, address){ override fun toString(): String { val start = "< BluetoothPebbleDevice, address=$address, connectionScopeActive=${connectionScope.value?.isActive}, bluetoothDevice=< BluetoothDevice address=${bluetoothDevice.address}" diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt index 838f4173..d82bddcc 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt @@ -7,9 +7,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.isActive class EmulatedPebbleDevice( - address: String, - protocolHandler: ProtocolHandler -) : PebbleDevice(null, protocolHandler, address){ + address: String +) : PebbleDevice(null, address){ override fun toString(): String { return "< EmulatedPebbleDevice, address=$address, connectionScopeActive=${connectionScope.value?.isActive} >" diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index fa15cfc7..b2e45b81 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -2,8 +2,8 @@ package io.rebble.cobble.bluetooth.ble import android.content.Context import io.rebble.cobble.bluetooth.* -import io.rebble.cobble.bluetooth.workarounds.UnboundWatchBeforeConnecting -import io.rebble.cobble.bluetooth.workarounds.WorkaroundDescriptor +import io.rebble.cobble.shared.workarounds.UnboundWatchBeforeConnecting +import io.rebble.cobble.shared.workarounds.WorkaroundDescriptor import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.* @@ -24,7 +24,7 @@ import kotlin.coroutines.CoroutineContext class BlueLEDriver( coroutineContext: CoroutineContext = Dispatchers.IO, private val context: Context, - private val protocolHandler: ProtocolHandler, + private val pebbleDevice: PebbleDevice, private val gattServerManager: GattServerManager, private val incomingPacketsListener: MutableSharedFlow, private val workaroundResolver: (WorkaroundDescriptor) -> Boolean @@ -45,7 +45,9 @@ class BlueLEDriver( } check(gattServer.state.value == NordicGattServer.State.OPEN) { "GATT server is not open" } - var gatt: BlueGATTConnection = device.bluetoothDevice.connectGatt(context, workaroundResolver(UnboundWatchBeforeConnecting)) + var gatt: BlueGATTConnection = device.bluetoothDevice.connectGatt(context, workaroundResolver( + UnboundWatchBeforeConnecting + )) ?: throw IOException("Failed to connect to device") try { emit(SingleConnectionStatus.Connecting(device)) @@ -77,7 +79,7 @@ class BlueLEDriver( val protocolIO = ProtocolIO( protocolInputStream.buffered(8192), protocolOutputStream.buffered(8192), - protocolHandler, + pebbleDevice.protocolHandler, incomingPacketsListener ) try { @@ -97,7 +99,7 @@ class BlueLEDriver( }?.flowOn(Dispatchers.IO)?.launchIn(scope) ?: throw IOException("Failed to get rxFlow") val sendLoop = scope.launch(Dispatchers.IO) { - protocolHandler.startPacketSendingLoop { + pebbleDevice.protocolHandler.startPacketSendingLoop { gattServer.sendMessageToDevice(device.address, it.asByteArray()) return@startPacketSendingLoop true } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt index aa1a4f3f..b7a18416 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt @@ -9,50 +9,52 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import java.io.IOException import java.util.UUID @Suppress("BlockingMethodInNonBlockingContext") class BlueSerialDriver( - private val protocolHandler: ProtocolHandler, + private val protocolHandler: PebbleDevice, private val incomingPacketsListener: MutableSharedFlow ) : BlueIO { private var protocolIO: ProtocolIO? = null @FlowPreview @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - override fun startSingleWatchConnection(device: PebbleDevice): Flow = flow { + override fun startSingleWatchConnection(device: PebbleDevice): Flow = flow { require(device is BluetoothPebbleDevice) { "Device must be BluetoothPebbleDevice" } - coroutineScope { - emit(SingleConnectionStatus.Connecting(device)) + emit(SingleConnectionStatus.Connecting(device)) - val btSerialUUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb") - val serialSocket = withContext(Dispatchers.IO) { - device.bluetoothDevice.createRfcommSocketToServiceRecord(btSerialUUID).also { - it.connect() - } + val btSerialUUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb") + val serialSocket = withContext(Dispatchers.IO) { + device.bluetoothDevice.createRfcommSocketToServiceRecord(btSerialUUID).also { + it.connect() } + } - val sendLoop = launch(CoroutineName("SendLoop")) { - protocolHandler.startPacketSendingLoop(::sendPacket) - } + val sendLoop = device.negotiationScope.launch(CoroutineName("SendLoop")) { + device.protocolHandler.startPacketSendingLoop(::sendPacket) + } + sendLoop.invokeOnCompletion { + serialSocket.close() + } - emit(SingleConnectionStatus.Connected(device)) + emit(SingleConnectionStatus.Connected(device)) - protocolIO = ProtocolIO( - serialSocket.inputStream, - serialSocket.outputStream, - protocolHandler, - incomingPacketsListener - ) + protocolIO = ProtocolIO( + serialSocket.inputStream, + serialSocket.outputStream, + device.protocolHandler, + incomingPacketsListener + ) - protocolIO!!.readLoop() - try { - serialSocket?.close() - } catch (e: IOException) { - } - sendLoop.cancel() + protocolIO!!.readLoop() + try { + serialSocket?.close() + } catch (e: IOException) { } + sendLoop.cancel() } private suspend fun sendPacket(bytes: UByteArray): Boolean { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt index a0261f08..4fc1771d 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt @@ -22,7 +22,7 @@ import kotlin.coroutines.coroutineContext * Used for testing app via a qemu pebble */ class SocketSerialDriver( - private val protocolHandler: ProtocolHandler, + private val device: PebbleDevice, private val incomingPacketsListener: MutableSharedFlow ) : BlueIO { @@ -63,7 +63,7 @@ class SocketSerialDriver( val packet = ByteArray(length.toInt() + 2 * (Short.SIZE_BYTES)) buf.get(packet, 0, packet.size) incomingPacketsListener.emit(packet) - protocolHandler.receivePacket(packet.toUByteArray()) + device.protocolHandler.receivePacket(packet.toUByteArray()) } } finally { Timber.e("Read loop returning") @@ -103,7 +103,7 @@ class SocketSerialDriver( delay(8000) val sendLoop = launch { - protocolHandler.startPacketSendingLoop(::sendPacket) + device.protocolHandler.startPacketSendingLoop(::sendPacket) } inputStream = serialSocket.inputStream diff --git a/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlowablePreference.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore/FlowablePreference.kt similarity index 89% rename from android/app/src/main/kotlin/io/rebble/cobble/datasources/FlowablePreference.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore/FlowablePreference.kt index 92578424..7e2ede66 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlowablePreference.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore/FlowablePreference.kt @@ -1,12 +1,10 @@ -package io.rebble.cobble.datasources +package io.rebble.cobble.shared.datastore import android.content.SharedPreferences import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.first @OptIn(ExperimentalCoroutinesApi::class) inline fun SharedPreferences.flow( diff --git a/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore/FlutterPreferences.kt similarity index 84% rename from android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore/FlutterPreferences.kt index 52f0e367..4ee01739 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/datastore/FlutterPreferences.kt @@ -1,26 +1,22 @@ -package io.rebble.cobble.datasources +package io.rebble.cobble.shared.datastore import android.content.Context import android.content.SharedPreferences import android.util.Base64 -import dagger.Reusable -import io.rebble.cobble.bluetooth.workarounds.WorkaroundDescriptor -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow +import io.rebble.cobble.shared.workarounds.WorkaroundDescriptor +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import java.io.ByteArrayInputStream import java.io.ObjectInputStream -import javax.inject.Inject /** * Read only provider for all shared preferences from flutter */ -@Reusable -class FlutterPreferences @Inject constructor(private val context: Context) { +class FlutterPreferences: KoinComponent { + private val context: Context by inject() private val preferences = context.getSharedPreferences( "FlutterSharedPreferences", - Context.MODE_PRIVATE + Context.MODE_PRIVATE ) val mutePhoneNotificationSounds = preferences.flow( diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt index bf027a61..6c5b49b8 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt @@ -4,13 +4,15 @@ import android.service.notification.StatusBarNotification import com.benasher44.uuid.Uuid import io.rebble.cobble.shared.AndroidPlatformContext import io.rebble.cobble.shared.PlatformContext +import io.rebble.cobble.shared.datastore.FlutterPreferences import io.rebble.cobble.shared.datastore.createDataStore 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.handlers.CalendarHandler -import io.rebble.cobble.shared.handlers.CobbleHandler +import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.cobble.shared.domain.notifications.NotificationProcessor +import io.rebble.cobble.shared.handlers.* import io.rebble.cobble.shared.handlers.music.MusicHandler import io.rebble.cobble.shared.jobs.AndroidJobScheduler import kotlinx.coroutines.flow.MutableStateFlow @@ -31,14 +33,27 @@ val androidModule = module { MutableStateFlow>(emptyMap()) } bind StateFlow::class single { AndroidJobScheduler() } + single { FlutterPreferences() } singleOf(::AndroidNotificationActionExecutor) singleOf(::AndroidCalendarActionExecutor) factory>(named("deviceHandlers")) { params -> - inject(named("commonDeviceHandlers")).value + - setOf( - CalendarHandler(params.get()), - MusicHandler(params.get()) - ) + val pebbleDevice: PebbleDevice = params.get() + setOf( + AppRunStateHandler(pebbleDevice), + AppInstallHandler(pebbleDevice), + CalendarActionHandler(pebbleDevice), + CalendarHandler(pebbleDevice), + MusicHandler(pebbleDevice) + ) } + + factory>(named("negotiationDeviceHandlers")) { params -> + val pebbleDevice: PebbleDevice = params.get() + setOf( + SystemHandler(pebbleDevice) + ) + } + + singleOf(::NotificationProcessor) } \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt index cdb79080..5a9c3f7a 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.* * Bus that triggers whenever permissions change */ object PermissionChangeBus { - private val _permissionChangeFlow = MutableSharedFlow(Channel.CONFLATED) + private val _permissionChangeFlow = MutableSharedFlow() val permissionChangeFlow = _permissionChangeFlow.asSharedFlow() fun trigger() { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/notifications/CallNotificationProcessor.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/CallNotificationProcessor.kt similarity index 89% rename from android/app/src/main/kotlin/io/rebble/cobble/notifications/CallNotificationProcessor.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/CallNotificationProcessor.kt index 459f0771..6372c0c7 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/notifications/CallNotificationProcessor.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/CallNotificationProcessor.kt @@ -1,7 +1,6 @@ -package io.rebble.cobble.notifications +package io.rebble.cobble.shared.domain.notifications import android.service.notification.StatusBarNotification -import io.rebble.cobble.errors.GlobalExceptionHandler import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.datastore.KMPPrefs import io.rebble.cobble.shared.domain.notifications.calls.CallNotification @@ -10,23 +9,20 @@ import io.rebble.cobble.shared.domain.notifications.calls.DiscordCallNotificatio import io.rebble.cobble.shared.domain.notifications.calls.WhatsAppCallNotificationInterpreter import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.libpebblecommon.packets.PhoneControl -import io.rebble.libpebblecommon.services.PhoneControlService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import javax.inject.Inject import kotlin.random.Random -import io.rebble.cobble.bluetooth.ConnectionLooper -import javax.inject.Singleton +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull +import io.rebble.cobble.shared.errors.GlobalExceptionHandler +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject -@Singleton -class CallNotificationProcessor @Inject constructor( - exceptionHandler: GlobalExceptionHandler, - private val prefs: KMPPrefs, - private val phoneControl: PhoneControlService, - private val connectionLooper: ConnectionLooper -) { +class CallNotificationProcessor: KoinComponent { + private val exceptionHandler: GlobalExceptionHandler by inject() + private val prefs: KMPPrefs by inject() val coroutineScope = CoroutineScope( SupervisorJob() + exceptionHandler ) @@ -57,10 +53,11 @@ class CallNotificationProcessor @Inject constructor( } else { Logging.d("Call state changed to ${state::class.simpleName} from ${previousState::class.simpleName}") } + val phoneControl = ConnectionStateManager.connectionState.value.watchOrNull?.phoneControlService if (state is CallState.RINGING && previousState is CallState.IDLE) { state.cookie?.let { Logging.d("Sending incoming call notification") - phoneControl.send( + phoneControl?.send( PhoneControl.IncomingCall( it, state.notification.contactHandle ?: "Unknown", @@ -73,13 +70,13 @@ class CallNotificationProcessor @Inject constructor( } else if (state is CallState.ONGOING && (previousState is CallState.RINGING || previousState is CallState.IDLE)) { state.cookie?.let { Logging.d("Sending start call") - phoneControl.send(PhoneControl.Start(it)) + phoneControl?.send(PhoneControl.Start(it)) } ?: run { Logging.e("Ongoing call state does not have a cookie") } } else if (state is CallState.IDLE && (previousState is CallState.ONGOING || previousState is CallState.RINGING)) { previousState.cookie?.let { - phoneControl.send(PhoneControl.End(it)) + phoneControl?.send(PhoneControl.End(it)) } ?: run { Logging.d("Previous call state does not have a cookie, not sending end call notification") } @@ -135,7 +132,10 @@ class CallNotificationProcessor @Inject constructor( // Random number that does not end with 0xCA (magic number for phone call) callState.value = CallState.RINGING(callNotification, nwCookie) } else if (callState.value !is CallState.ONGOING && callNotification.type == CallNotificationType.ONGOING) { - callState.value = CallState.ONGOING(callNotification, (callState.value as? CallState.RINGING)?.cookie ?: nwCookie) + callState.value = CallState.ONGOING( + callNotification, + (callState.value as? CallState.RINGING)?.cookie ?: nwCookie + ) } } } @@ -158,7 +158,7 @@ class CallNotificationProcessor @Inject constructor( } fun handleCallAction(action: PhoneControl) { - if (connectionLooper.connectionState.value !is ConnectionState.Connected) { + if (ConnectionStateManager.connectionState.value !is ConnectionState.Connected) { Logging.w("Ignoring phone control message because watch is not connected") return } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/data/NotificationGroup.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationGroup.kt similarity index 93% rename from android/app/src/main/kotlin/io/rebble/cobble/data/NotificationGroup.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationGroup.kt index 321c55de..b7bd7b30 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/data/NotificationGroup.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationGroup.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.data +package io.rebble.cobble.shared.domain.notifications import android.service.notification.StatusBarNotification import androidx.core.app.NotificationCompat diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationListener.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationListener.kt index 03fad559..8a3a2f69 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationListener.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationListener.kt @@ -10,47 +10,38 @@ import android.service.notification.NotificationListenerService import android.service.notification.StatusBarNotification import androidx.core.app.NotificationCompat import com.benasher44.uuid.Uuid -import io.rebble.cobble.CobbleApplication -import io.rebble.cobble.bluetooth.ConnectionLooper -import io.rebble.cobble.data.toNotificationGroup -import io.rebble.cobble.datasources.FlutterPreferences import io.rebble.cobble.shared.database.dao.NotificationChannelDao +import io.rebble.cobble.shared.datastore.FlutterPreferences import io.rebble.cobble.shared.datastore.KMPPrefs import io.rebble.cobble.shared.domain.state.ConnectionState +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.errors.GlobalExceptionHandler import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.qualifier.named import timber.log.Timber -class NotificationListener : NotificationListenerService() { - private lateinit var coroutineScope: CoroutineScope - private lateinit var connectionLooper: ConnectionLooper - private lateinit var flutterPreferences: FlutterPreferences - private lateinit var notificationProcessor: NotificationProcessor - private lateinit var callNotificationProcessor: CallNotificationProcessor - private lateinit var activeNotifsState: MutableStateFlow> - private lateinit var notificationChannelDao: NotificationChannelDao +class NotificationListener : NotificationListenerService(), KoinComponent { + private val globalExceptionHandler: GlobalExceptionHandler by inject() + private val coroutineScope: CoroutineScope = CoroutineScope( + SupervisorJob() + globalExceptionHandler + CoroutineName("NotificationListener") + ) + private val notificationProcessor: NotificationProcessor by inject() + private val callNotificationProcessor: CallNotificationProcessor by inject() + private val activeNotifsState: MutableStateFlow> by inject(named("activeNotifsState")) + private val notificationChannelDao: NotificationChannelDao by inject() + //TODO: switch to main prefs once we switch notif pages to flutter + private val flutterPreferences: FlutterPreferences by inject() + private val prefs: KMPPrefs by inject() + private var isListening = false private var areNotificationsEnabled = true private var mutedPackages = listOf() - private lateinit var prefs: KMPPrefs - override fun onCreate() { - val injectionComponent = (applicationContext as CobbleApplication).component - - coroutineScope = CoroutineScope( - SupervisorJob() + injectionComponent.createExceptionHandler() + CoroutineName("NotificationListener") - ) - - connectionLooper = injectionComponent.createConnectionLooper() - flutterPreferences = injectionComponent.createFlutterPreferences() - prefs = injectionComponent.createKMPPrefs() - notificationProcessor = injectionComponent.createNotificationProcessor() - activeNotifsState = injectionComponent.createActiveNotifsState() - notificationChannelDao = injectionComponent.createNotificationChannelDao() - callNotificationProcessor = injectionComponent.createCallNotificationProcessor() - super.onCreate() _isActive.value = true Timber.d("NotificationListener created") @@ -199,7 +190,7 @@ class NotificationListener : NotificationListenerService() { combine( flutterPreferences.mutePhoneNotificationSounds, flutterPreferences.mutePhoneCallSounds, - connectionLooper.connectionState + ConnectionStateManager.connectionState ) { mutePhoneNotificationSounds, mutePhoneCallSounds, connectionState -> if (connectionState is ConnectionState.Disconnected) { // Do nothing. Listener will be unbound anyway @@ -208,24 +199,13 @@ class NotificationListener : NotificationListenerService() { val connected = connectionState is ConnectionState.Connected - val listenerHints = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - var hints = 0 - - if (connected && mutePhoneNotificationSounds) { - hints = hints or HINT_HOST_DISABLE_NOTIFICATION_EFFECTS - } - - if (connected && mutePhoneCallSounds) { - hints = hints or HINT_HOST_DISABLE_CALL_EFFECTS - } + var listenerHints = 0 + if (connected && mutePhoneNotificationSounds) { + listenerHints = listenerHints or HINT_HOST_DISABLE_NOTIFICATION_EFFECTS + } - hints - } else { - if (connected && (mutePhoneCallSounds || mutePhoneNotificationSounds)) { - HINT_HOST_DISABLE_EFFECTS - } else { - 0 - } + if (connected && mutePhoneCallSounds) { + listenerHints = listenerHints or HINT_HOST_DISABLE_CALL_EFFECTS } requestListenerHints(listenerHints) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/data/NotificationMessage.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationMessage.kt similarity index 73% rename from android/app/src/main/kotlin/io/rebble/cobble/data/NotificationMessage.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationMessage.kt index 937fc844..381956c0 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/data/NotificationMessage.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationMessage.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.data +package io.rebble.cobble.shared.domain.notifications import kotlinx.serialization.Serializable diff --git a/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationProcessor.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationProcessor.kt similarity index 93% rename from android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationProcessor.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationProcessor.kt index ce713d24..af0d91d9 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationProcessor.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationProcessor.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.notifications +package io.rebble.cobble.shared.domain.notifications import android.app.Notification import android.app.Notification.Action @@ -8,20 +8,16 @@ import android.service.notification.StatusBarNotification import androidx.core.app.NotificationCompat import com.benasher44.uuid.uuid4 import com.benasher44.uuid.uuidFrom -import com.benasher44.uuid.uuidOf -import io.rebble.cobble.data.NotificationGroup -import io.rebble.cobble.data.NotificationMessage import io.rebble.cobble.shared.database.dao.NotificationChannelDao import io.rebble.cobble.shared.database.dao.PersistedNotificationDao -import io.rebble.cobble.shared.database.entity.NotificationChannel import io.rebble.cobble.shared.database.entity.PersistedNotification import io.rebble.cobble.shared.datastore.DEFAULT_MUTED_PACKAGES_VERSION import io.rebble.cobble.shared.datastore.KMPPrefs import io.rebble.cobble.shared.datastore.defaultMutedPackages import io.rebble.cobble.shared.domain.common.SystemAppIDs.notificationsWatchappId -import io.rebble.cobble.shared.domain.notifications.* -import io.rebble.cobble.shared.domain.state.ConnectionState +import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull +import io.rebble.cobble.shared.errors.GlobalExceptionHandler import io.rebble.libpebblecommon.PacketPriority import io.rebble.libpebblecommon.packets.blobdb.BlobCommand import io.rebble.libpebblecommon.packets.blobdb.BlobResponse @@ -36,31 +32,24 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.actor import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first -import okio.Timeout +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import org.koin.core.qualifier.named -import org.koin.mp.KoinPlatformTools import timber.log.Timber import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton import kotlin.random.Random import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds -@Singleton -class NotificationProcessor @Inject constructor( - exceptionHandler: CoroutineExceptionHandler, - private val persistedNotifDao: PersistedNotificationDao, - private val notificationChannelDao: NotificationChannelDao, - private val context: Context, - private val activeNotifsState: MutableStateFlow>, - private val prefs: KMPPrefs, -) { - //TODO: Use Koin for DI - private val connectionState: StateFlow = KoinPlatformTools.defaultContext().get().get(named("connectionState")) +class NotificationProcessor: KoinComponent { + private val exceptionHandler: GlobalExceptionHandler by inject() + private val persistedNotifDao: PersistedNotificationDao by inject() + private val notificationChannelDao: NotificationChannelDao by inject() + private val context: Context by inject() + private val activeNotifsState: MutableStateFlow> by inject(named("activeNotifsState")) + private val prefs: KMPPrefs by inject() companion object { private val notificationProcessingTimeout = 10.seconds } @@ -68,7 +57,7 @@ class NotificationProcessor @Inject constructor( SupervisorJob() + exceptionHandler + CoroutineName("NotificationProcessor") ) - private val blobDBService: BlobDBService? get() = connectionState.value.watchOrNull?.blobDBService + private val blobDBService: BlobDBService? get() = ConnectionStateManager.connectionState.value.watchOrNull?.blobDBService private val activeGroups = mutableMapOf() diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/CalendarHandler.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/CalendarHandler.kt index 35f5e854..87842e5c 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/CalendarHandler.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/CalendarHandler.kt @@ -9,12 +9,14 @@ import android.provider.CalendarContract import androidx.core.content.ContextCompat import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager +import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.datastore.KMPPrefs import io.rebble.cobble.shared.domain.PermissionChangeBus import io.rebble.cobble.shared.domain.calendar.CalendarSync import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.jobs.CalendarSyncWorker import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* import kotlinx.coroutines.job import kotlinx.coroutines.launch @@ -33,11 +35,13 @@ class CalendarHandler(private val pebbleDevice: PebbleDevice) : CobbleHandler, K private var initialSyncJob: Job? = null private var calendarHandlerStarted = false - private val calendarChangeFlow = MutableSharedFlow() + private val calendarChangeFlow = MutableSharedFlow(extraBufferCapacity = 4, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val contentObserver = object : ContentObserver(null) { override fun onChange(selfChange: Boolean, uri: Uri?) { - calendarChangeFlow.tryEmit(Unit) + if (!calendarChangeFlow.tryEmit(Unit)) { + Logging.e("Failed to emit calendar change event") + } } } diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/ActiveMediaSessionProvider.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/ActiveMediaSessionProvider.kt index 6a6b038c..26da173d 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/ActiveMediaSessionProvider.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/ActiveMediaSessionProvider.kt @@ -4,11 +4,10 @@ import android.content.Context import android.media.session.MediaController import android.media.session.MediaSessionManager import android.media.session.PlaybackState -import io.rebble.cobble.notifications.NotificationListener +import io.rebble.cobble.shared.domain.notifications.NotificationListener import org.koin.core.component.KoinComponent import org.koin.core.component.inject import timber.log.Timber -import javax.inject.Inject class ActiveMediaSessionProvider : androidx.lifecycle.LiveData(), diff --git a/android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/providers/PebbleKitProvider.kt similarity index 84% rename from android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/providers/PebbleKitProvider.kt index 9fd82291..316497c7 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/providers/PebbleKitProvider.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.providers +package io.rebble.cobble.shared.providers import android.content.ContentProvider import android.content.ContentValues @@ -6,10 +6,9 @@ import android.database.Cursor import android.database.MatrixCursor import android.net.Uri import com.getpebble.android.kit.Constants -import io.rebble.cobble.CobbleApplication -import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -19,8 +18,6 @@ import kotlinx.coroutines.launch class PebbleKitProvider : ContentProvider() { private var initialized = false - private lateinit var connectionLooper: ConnectionLooper - override fun onCreate(): Boolean { // Do not initialize anything here as this gets called before Application.onCreate @@ -38,24 +35,19 @@ class PebbleKitProvider : ContentProvider() { initialized = true - val injectionComponent = (context as CobbleApplication) - .component - - connectionLooper = injectionComponent.createConnectionLooper() - GlobalScope.launch(Dispatchers.Main.immediate) { - connectionLooper.connectionState.collect { + ConnectionStateManager.connectionState.collect { context.contentResolver.notifyChange(Constants.URI_CONTENT_BASALT, null) } } } override fun query( - uri: Uri, - projection: Array?, - selection: String?, - selectionArgs: Array?, - sortOrder: String? + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? ): Cursor? { if (uri != Constants.URI_CONTENT_BASALT) { return null @@ -65,9 +57,9 @@ class PebbleKitProvider : ContentProvider() { val cursor = MatrixCursor(CURSOR_COLUMN_NAMES) - val metadata = ConnectionStateManager.connectedWatchMetadata.value + val metadata = ConnectionStateManager.connectionState.value.watchOrNull?.metadata?.value - if (connectionLooper.connectionState.value is ConnectionState.Connected && + if (ConnectionStateManager.connectionState.value is ConnectionState.Connected && metadata != null) { val parsedVersion = FIRMWARE_VERSION_REGEX.find(metadata.running.versionTag.get()) val groupValues = parsedVersion?.groupValues diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/workarounds/UnboundWatchBeforeConnecting.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/workarounds/UnboundWatchBeforeConnecting.kt similarity index 86% rename from android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/workarounds/UnboundWatchBeforeConnecting.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/workarounds/UnboundWatchBeforeConnecting.kt index c372fa5a..faf3f497 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/workarounds/UnboundWatchBeforeConnecting.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/workarounds/UnboundWatchBeforeConnecting.kt @@ -1,8 +1,8 @@ -package io.rebble.cobble.bluetooth.workarounds +package io.rebble.cobble.shared.workarounds import android.content.Context import android.os.Build -import io.rebble.cobble.bluetooth.workarounds.UnboundWatchBeforeConnecting.isNeeded +import io.rebble.cobble.shared.workarounds.UnboundWatchBeforeConnecting.isNeeded /** * Workaround for BT stack bug on Android 9 and 10 where phone can't connect to the watch if it was diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/workarounds/WorkaroundDescriptor.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/workarounds/WorkaroundDescriptor.kt similarity index 85% rename from android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/workarounds/WorkaroundDescriptor.kt rename to android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/workarounds/WorkaroundDescriptor.kt index c1485f00..56738f7c 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/workarounds/WorkaroundDescriptor.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/workarounds/WorkaroundDescriptor.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.bluetooth.workarounds +package io.rebble.cobble.shared.workarounds import android.content.Context diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/CalendarModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/CalendarModule.kt index ceda4c99..23d08162 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/CalendarModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/CalendarModule.kt @@ -1,8 +1,13 @@ package io.rebble.cobble.shared.di +import io.rebble.cobble.shared.domain.calendar.CalendarSync import io.rebble.cobble.shared.domain.calendar.PhoneCalendarSyncer import io.rebble.cobble.shared.domain.timeline.WatchTimelineSyncer import io.rebble.cobble.shared.domain.timeline.TimelineActionManager +import io.rebble.cobble.shared.errors.GlobalExceptionHandler +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import org.koin.core.module.dsl.singleOf import org.koin.dsl.module @@ -10,4 +15,7 @@ val calendarModule = module { singleOf(::PhoneCalendarSyncer) singleOf(::WatchTimelineSyncer) singleOf(::TimelineActionManager) + single { + CalendarSync(CoroutineScope(SupervisorJob() + get() + CoroutineName("CalendarSync"))) + } } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DatabaseModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DatabaseModule.kt index bf11c2ca..2d3733d4 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DatabaseModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DatabaseModule.kt @@ -10,4 +10,6 @@ val databaseModule = module { single { get().calendarDao() } single { get().timelinePinDao() } single { get().lockerDao() } + single { get().notificationChannelDao() } + single { get().persistedNotificationDao() } } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DependenciesModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DependenciesModule.kt index 37fc1350..0f7e76c4 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DependenciesModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DependenciesModule.kt @@ -6,6 +6,7 @@ import io.ktor.client.plugins.cache.storage.CacheStorage import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json import io.rebble.cobble.shared.PlatformContext +import io.rebble.cobble.shared.errors.GlobalExceptionHandler import org.koin.dsl.module val dependenciesModule = module { @@ -19,6 +20,8 @@ val dependenciesModule = module { } } } + + single { GlobalExceptionHandler() } } expect fun makePlatformCacheStorage(platformContext: PlatformContext): CacheStorage \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt index a41f4976..e621c6bc 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt @@ -1,34 +1,33 @@ package io.rebble.cobble.shared.di +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.middleware.PutBytesController import io.rebble.libpebblecommon.ProtocolHandlerImpl import io.rebble.libpebblecommon.ProtocolHandler -import io.rebble.libpebblecommon.services.AppFetchService -import io.rebble.libpebblecommon.services.MusicService -import io.rebble.libpebblecommon.services.PutBytesService -import io.rebble.libpebblecommon.services.SystemService +import io.rebble.libpebblecommon.services.* import io.rebble.libpebblecommon.services.app.AppRunStateService import io.rebble.libpebblecommon.services.appmessage.AppMessageService import io.rebble.libpebblecommon.services.blobdb.BlobDBService import org.koin.dsl.module import io.rebble.libpebblecommon.services.blobdb.TimelineService +import org.koin.dsl.bind val libpebbleModule = module { - //TODO: Move away from global protocol handler and singleton services - single { + factory { ProtocolHandlerImpl() + } bind ProtocolHandler::class + + factory { params -> + TimelineService(params.get()) } - single { - TimelineService(get()) - } - single { - PutBytesService(get()) + factory { params -> + PutBytesService(params.get()) } - single { - AppFetchService(get()) + factory { params -> + AppFetchService(params.get()) } - single { - PutBytesController() + factory { params -> + PutBytesController(params.get()) } factory { params -> @@ -50,4 +49,20 @@ val libpebbleModule = module { factory { params -> SystemService(params.get()) } + + factory { params -> + PhoneControlService(params.get()) + } + + factory { params -> + AppLogService(params.get()) + } + + factory { params -> + LogDumpService(params.get()) + } + + factory { params -> + ScreenshotService(params.get()) + } } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/PebbleDeviceModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/PebbleDeviceModule.kt deleted file mode 100644 index d56a68bf..00000000 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/PebbleDeviceModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.rebble.cobble.shared.di - -import io.rebble.cobble.shared.handlers.* -import org.koin.core.qualifier.named -import org.koin.dsl.module - -val pebbleDeviceModule = module { - - factory>(named("commonNegotiationDeviceHandlers")) { params -> - setOf( - SystemHandler(params.get()), - ) - } - factory>(named("commonDeviceHandlers")) { params -> - get>(named("commonNegotiationDeviceHandlers")) + - setOf( - SystemHandler(params.get()), - AppRunStateHandler(params.get()), - AppInstallHandler(params.get()), - CalendarActionHandler(params.get()) - ) - } -} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt index 9bd5a92d..9528a580 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt @@ -15,12 +15,6 @@ val stateModule = module { single(named("connectionState")) { MutableStateFlow(ConnectionState.Disconnected) } bind StateFlow::class - factory(named("connectedWatchMetadata")) { - get>(named("connectionState")) - .flatMapLatest { it.watchOrNull?.metadata?.take(1) ?: flowOf(null) } - .filterNotNull() - .stateIn(CoroutineScope(Dispatchers.Default), SharingStarted.WhileSubscribed(), null) - } factory(named("isConnected")) { get>(named("connectionState")) .map { it is ConnectionState.Connected } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt index 06c1bb2f..8f1bfaab 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt @@ -2,34 +2,56 @@ package io.rebble.cobble.shared.domain.common import com.benasher44.uuid.Uuid import io.ktor.http.parametersOf +import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.domain.state.ConnectionState +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull +import io.rebble.cobble.shared.handlers.CobbleHandler +import io.rebble.cobble.shared.middleware.PutBytesController import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.packets.WatchVersion -import io.rebble.libpebblecommon.services.MusicService -import io.rebble.libpebblecommon.services.SystemService +import io.rebble.libpebblecommon.services.* import io.rebble.libpebblecommon.services.app.AppRunStateService import io.rebble.libpebblecommon.services.appmessage.AppMessageService import io.rebble.libpebblecommon.services.blobdb.BlobDBService -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel +import io.rebble.libpebblecommon.services.blobdb.TimelineService +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koin.core.parameter.parametersOf +import org.koin.core.qualifier.named open class PebbleDevice( metadata: WatchVersion.WatchVersionResponse?, - private val protocolHandler: ProtocolHandler, val address: String, ): KoinComponent, AutoCloseable { + private val negotiationHandlers: Set by inject(named("negotiationDeviceHandlers")) { parametersOf(this as PebbleDevice) } + private val handlers: Set by inject(named("deviceHandlers")) { parametersOf(this as PebbleDevice) } + + val protocolHandler: ProtocolHandler by inject() val negotiationScope = CoroutineScope(Dispatchers.Default + CoroutineName("NegotationScope-$address")) val metadata: MutableStateFlow = MutableStateFlow(metadata) val modelId: MutableStateFlow = MutableStateFlow(null) val connectionScope: MutableStateFlow = MutableStateFlow(null) val currentActiveApp: MutableStateFlow = MutableStateFlow(null) + init { + // This will init all the handlers by reading the lazy value causing them to be injected + negotiationScope.launch { + val initNHandlers = negotiationHandlers.joinToString { it::class.simpleName ?: "Unknown" } + Logging.i("Initialised negotiation handlers: $initNHandlers") + ConnectionStateManager.connectionState.first { it is ConnectionState.Connected && it.watch.address == address } + val connectionScope = connectionScope.filterNotNull().first() + connectionScope.launch { + val initHandlers = handlers.joinToString { it::class.simpleName ?: "Unknown" } + Logging.i("Initialised handlers: $initHandlers") + } + } + } + override fun toString(): String = "< PebbleDevice address=$address >" //TODO: Move to per-protocol handler services, so we can have multiple PebbleDevices, this is the first of many @@ -38,8 +60,19 @@ open class PebbleDevice( val appMessageService: AppMessageService by inject {parametersOf(protocolHandler)} val systemService: SystemService by inject {parametersOf(protocolHandler)} val musicService: MusicService by inject {parametersOf(protocolHandler)} + val putBytesService: PutBytesService by inject {parametersOf(protocolHandler)} + val phoneControlService: PhoneControlService by inject {parametersOf(protocolHandler)} + val appLogsService: AppLogService by inject {parametersOf(protocolHandler)} + val logDumpService: LogDumpService by inject {parametersOf(protocolHandler)} + val screenshotService: ScreenshotService by inject {parametersOf(protocolHandler)} + val timelineService: TimelineService by inject {parametersOf(protocolHandler)} + val appFetchService: AppFetchService by inject {parametersOf(protocolHandler)} + + val putBytesController: PutBytesController by inject {parametersOf(this)} override fun close() { negotiationScope.cancel("PebbleDevice closed") + connectionScope.value?.cancel("PebbleDevice closed") + connectionScope.value = null } } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt index 347349e4..9afebcbc 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt @@ -31,10 +31,5 @@ val ConnectionState.watchOrNull: PebbleDevice? object ConnectionStateManager: KoinComponent { val connectionState: MutableStateFlow by inject(named("connectionState")) - /** - * Flow of the currently connected watch's metadata. This flow only emits when a watch is connected and will not emit if negotiation never completes. - */ - val connectedWatchMetadata: StateFlow by inject(named("connectedWatchMetadata")) - val isConnected: StateFlow by inject(named("isConnected")) } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/TimelineActionManager.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/TimelineActionManager.kt index f8749286..92226d63 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/TimelineActionManager.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/TimelineActionManager.kt @@ -5,6 +5,7 @@ import io.rebble.cobble.shared.Logging import org.koin.core.component.KoinComponent import org.koin.core.component.inject import io.rebble.cobble.shared.database.dao.TimelinePinDao +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.packets.blobdb.TimelineAction import io.rebble.libpebblecommon.services.blobdb.TimelineService import kotlinx.coroutines.CompletableDeferred @@ -13,9 +14,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* -class TimelineActionManager: KoinComponent { +class TimelineActionManager(private val pebbleDevice: PebbleDevice): KoinComponent { private val timelineDao: TimelinePinDao by inject() - private val timelineService: TimelineService by inject() + private val timelineService = pebbleDevice.timelineService private val scope = CoroutineScope(Dispatchers.Default) //todo: Exception handler val actionFlow = callbackFlow { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/errors/GlobalErrorHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/errors/GlobalErrorHandler.kt similarity index 68% rename from android/app/src/main/kotlin/io/rebble/cobble/errors/GlobalErrorHandler.kt rename to android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/errors/GlobalErrorHandler.kt index 6f03dd43..b67c120b 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/errors/GlobalErrorHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/errors/GlobalErrorHandler.kt @@ -1,22 +1,21 @@ -package io.rebble.cobble.errors +package io.rebble.cobble.shared.errors +import io.rebble.cobble.shared.Logging import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineExceptionHandler -import timber.log.Timber -import javax.inject.Inject import kotlin.coroutines.CoroutineContext /** * Global exception handler for all coroutines in the app */ -class GlobalExceptionHandler @Inject constructor() : CoroutineExceptionHandler { +class GlobalExceptionHandler: CoroutineExceptionHandler { override fun handleException(context: CoroutineContext, exception: Throwable) { if (exception is CancellationException) { return } // TODO properly handle exceptions (logging?) - Timber.e(exception, "Coroutine exception - context: $context") + Logging.e("Coroutine exception - context: $context", exception) } override val key: CoroutineContext.Key<*> diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt index b0f8b5b8..fe2bce12 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt @@ -31,13 +31,13 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject class AppInstallHandler( - pebbleDevice: PebbleDevice + private val pebbleDevice: PebbleDevice ): CobbleHandler, KoinComponent { private val lockerDao: LockerDao by inject() private val platformContext: PlatformContext by inject() - private val appFetchService: AppFetchService by inject() private val httpClient: HttpClient by inject() - private val putBytesController: PutBytesController by inject() + private val putBytesController = pebbleDevice.putBytesController + private val appFetchService = pebbleDevice.appFetchService init { pebbleDevice.negotiationScope.launch { @@ -99,7 +99,7 @@ class AppInstallHandler( // Wait some time for metadata to become available in case this has been called // Right after watch has been connected val hardwarePlatformNumber = withTimeoutOrNull(2_000) { - ConnectionStateManager.connectedWatchMetadata.first() + pebbleDevice.metadata.first() } ?.running ?.hardwarePlatform diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt index 7ff16b7c..37656970 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt @@ -38,7 +38,10 @@ class SystemHandler( sendCurrentTime() } - negotiate() + negotiationScope.launch { + ConnectionStateManager.connectionState.first { it is ConnectionState.Negotiating } + negotiate() + } } fun negotiationsComplete(watch: PebbleDevice) { @@ -62,12 +65,12 @@ class SystemHandler( ConnectionStateManager.connectionState.first { it is ConnectionState.Negotiating } Logging.i("Negotiating with watch") refreshWatchMetadata() - ConnectionStateManager.connectedWatchMetadata.value?.let { + pebbleDevice.metadata.value?.let { if (it.running.isRecovery.get()) { Logging.i("Watch is in recovery mode, switching to recovery state") - ConnectionStateManager.connectionState.value.watchOrNull?.let { it1 -> recoveryMode(it1) } + recoveryMode(pebbleDevice) } else { - ConnectionStateManager.connectionState.value.watchOrNull?.let { it1 -> negotiationsComplete(it1) } + negotiationsComplete(pebbleDevice) } } } @@ -80,10 +83,12 @@ class SystemHandler( withTimeout(3000) { val watchInfo = systemService.requestWatchVersion() //FIXME: Possible race condition here - val watch = ConnectionStateManager.connectionState.value.watchOrNull - watch?.metadata?.value = watchInfo + pebbleDevice.metadata.value = watchInfo val watchModel = systemService.requestWatchModel() - watch?.modelId?.value = watchModel + pebbleDevice.modelId.value = watchModel + } + if (retries > 0) { + Logging.i("Successfully got watch metadata after $retries retries") } break } catch (e: TimeoutCancellationException) { @@ -96,8 +101,7 @@ class SystemHandler( } if (retries >= 3) { Logging.e("Failed to get watch metadata after 3 retries, giving up and reconnecting") - //TODO: double check this works - negotiationScope.cancel("Failed to get watch metadata") + pebbleDevice.close() } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/middleware/AppLogController.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/AppLogController.kt similarity index 60% rename from android/app/src/main/kotlin/io/rebble/cobble/middleware/AppLogController.kt rename to android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/AppLogController.kt index 1112c40a..efe32203 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/middleware/AppLogController.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/AppLogController.kt @@ -1,28 +1,27 @@ -package io.rebble.cobble.middleware +package io.rebble.cobble.shared.middleware -import io.rebble.cobble.bluetooth.ConnectionLooper +import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.domain.state.ConnectionState +import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.libpebblecommon.packets.AppLogShippingControlMessage -import io.rebble.libpebblecommon.services.AppLogService import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext import timber.log.Timber -import javax.inject.Inject -class AppLogController @Inject constructor( - connectionLooper: ConnectionLooper, - private val appLogsService: AppLogService +class AppLogController( + private val pebbleDevice: PebbleDevice, ) { @OptIn(ExperimentalCoroutinesApi::class) - val logs = connectionLooper.connectionState.flatMapLatest { + val logs = ConnectionStateManager.connectionState.flatMapLatest { if (it is ConnectionState.Connected) { flow { toggleAppLogOnWatch(true) - emitAll(appLogsService.receivedMessages.receiveAsFlow()) + emitAll(pebbleDevice.appLogsService.receivedMessages.receiveAsFlow()) } } else { emptyFlow() @@ -34,10 +33,10 @@ class AppLogController @Inject constructor( private suspend fun toggleAppLogOnWatch(enable: Boolean) { try { withContext(NonCancellable) { - appLogsService.send(AppLogShippingControlMessage(enable)) + pebbleDevice.appLogsService.send(AppLogShippingControlMessage(enable)) } } catch (e: Exception) { - Timber.e(e, "AppLog transmit error") + Logging.e("AppLog transmit error", e) } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/middleware/DeviceLogController.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/DeviceLogController.kt similarity index 72% rename from android/app/src/main/kotlin/io/rebble/cobble/middleware/DeviceLogController.kt rename to android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/DeviceLogController.kt index 93ba17db..de8491e0 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/middleware/DeviceLogController.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/DeviceLogController.kt @@ -1,20 +1,17 @@ -package io.rebble.cobble.middleware +package io.rebble.cobble.shared.middleware -import io.rebble.cobble.bluetooth.ConnectionLooper -import io.rebble.cobble.shared.domain.state.ConnectionState -import io.rebble.libpebblecommon.packets.AppLogShippingControlMessage +import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.packets.LogDump -import io.rebble.libpebblecommon.services.LogDumpService import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex -import timber.log.Timber -import javax.inject.Inject import kotlin.random.Random -class DeviceLogController @Inject constructor( - private val deviceLogsService: LogDumpService +class DeviceLogController( + private val device: PebbleDevice ) { + private val deviceLogsService = device.logDumpService private val scope = CoroutineScope(Dispatchers.IO) private val mutex = Mutex() @@ -35,7 +32,7 @@ class DeviceLogController @Inject constructor( it !is LogDump.Done } .onCompletion { - Timber.d("Log dump completed") + Logging.d("Log dump completed") mutex.unlock() } deviceLogsService.send( diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt index 07051608..87a621d2 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt @@ -1,6 +1,7 @@ package io.rebble.cobble.shared.middleware import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.cobble.shared.util.File @@ -22,9 +23,8 @@ import okio.use import org.koin.core.component.KoinComponent import org.koin.core.component.inject -class PutBytesController: KoinComponent { - private val putBytesService: PutBytesService by inject() - +class PutBytesController(pebbleDevice: PebbleDevice): KoinComponent { + private val putBytesService = pebbleDevice.putBytesService private val _status: MutableStateFlow = MutableStateFlow(Status(State.IDLE)) private val statusMutex = Mutex() val status: StateFlow get() = _status diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt index a791fd61..4075eb38 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt @@ -10,6 +10,7 @@ import io.ktor.client.request.get import io.ktor.http.HttpStatusCode import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms +import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform @@ -28,11 +29,13 @@ class LockerItemViewModel(private val httpClient: HttpClient, val entry: SyncedL } private var _imageState = MutableStateFlow(ImageState.Loading) val imageState = _imageState.asStateFlow() - val supportedState = ConnectionStateManager.connectedWatchMetadata.map { - it?.running?.let { running -> - val platform = WatchHardwarePlatform.fromProtocolNumber(running.hardwarePlatform.get()) - entry.platforms.any { it.name == platform?.watchType?.codename } - } ?: true + val supportedState = ConnectionStateManager.connectionState.flatMapConcat { + it.watchOrNull?.metadata?.mapNotNull { meta -> + meta?.running?.let { running -> + val platform = WatchHardwarePlatform.fromProtocolNumber(running.hardwarePlatform.get()) + entry.platforms.any { it.name == platform?.watchType?.codename } + } + } ?: flowOf(true) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), true) val title: String @@ -62,8 +65,8 @@ class LockerItemViewModel(private val httpClient: HttpClient, val entry: SyncedL } init { - ConnectionStateManager.connectedWatchMetadata.onEach { - val platform = it?.running?.hardwarePlatform?.get()?.let { platformId -> + ConnectionStateManager.connectionState.filterIsInstance().onEach { + val platform = it.watch.metadata.value?.running?.hardwarePlatform?.get()?.let { platformId -> WatchHardwarePlatform.fromProtocolNumber(platformId) } val availablePlatform = platform?.let { entry.platforms.firstOrNull { it.name == platform.watchType.codename } } diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/di/IosModule.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/di/IosModule.kt index ebfc5f04..1ee13fae 100644 --- a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/di/IosModule.kt +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/di/IosModule.kt @@ -3,12 +3,28 @@ package io.rebble.cobble.shared.di import io.rebble.cobble.shared.IOSPlatformContext import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.datastore.createDataStore +import io.rebble.cobble.shared.handlers.* import org.koin.core.module.dsl.factoryOf +import org.koin.core.qualifier.named import org.koin.dsl.module val iosModule = module { - factoryOf { + factory { IOSPlatformContext() } single { createDataStore() } + + factory>(named("deviceHandlers")) { params -> + get>(named("negotiationDeviceHandlers")) + setOf( + AppRunStateHandler(params.get()), + AppInstallHandler(params.get()), + CalendarActionHandler(params.get()) + ) + } + + factory>(named("negotiationDeviceHandlers")) { params -> + setOf( + SystemHandler(params.get()) + ) + } } \ No newline at end of file From 936bfad01e256a1fd01a1b278217ffb87bc0d60f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 23 Sep 2024 17:28:24 +0100 Subject: [PATCH 10/20] add more logging to connection errors --- .../src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt | 2 +- .../io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt | 3 +++ .../io/rebble/cobble/shared/middleware/AppLogController.kt | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt index b76e0ae7..d252ea08 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt @@ -55,7 +55,7 @@ class ProtocolIO( protocolHandler.receivePacket(packet.toUByteArray()) } } finally { - Timber.e("Read loop returning") + Timber.e("Read loop returning: coroutineContext.isActive = ${coroutineContext.isActive}") try { inputStream.close() outputStream.close() diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt index b7a18416..7a78fb43 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt @@ -3,6 +3,7 @@ package io.rebble.cobble.bluetooth.classic import android.Manifest import androidx.annotation.RequiresPermission import io.rebble.cobble.bluetooth.* +import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.* @@ -37,6 +38,7 @@ class BlueSerialDriver( device.protocolHandler.startPacketSendingLoop(::sendPacket) } sendLoop.invokeOnCompletion { + Logging.e("Send loop completed", it) serialSocket.close() } @@ -51,6 +53,7 @@ class BlueSerialDriver( protocolIO!!.readLoop() try { + Logging.e("Closing socket post read loop: isConnected = ${serialSocket.isConnected}") serialSocket?.close() } catch (e: IOException) { } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/AppLogController.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/AppLogController.kt index efe32203..7d589501 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/AppLogController.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/AppLogController.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext -import timber.log.Timber class AppLogController( private val pebbleDevice: PebbleDevice, From 86792d664f5b50f6f46aeecd519b224a9d3b7225 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 24 Sep 2024 17:28:22 +0100 Subject: [PATCH 11/20] add call notif processor to di, test page trigger sync locker button --- .../kotlin/io/rebble/cobble/shared/di/AndroidModule.kt | 2 ++ .../io/rebble/cobble/shared/ui/view/home/TestPage.kt | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt index 6c5b49b8..f0a0408f 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt @@ -11,6 +11,7 @@ import io.rebble.cobble.shared.domain.notifications.PlatformNotificationActionEx import io.rebble.cobble.shared.domain.notifications.AndroidNotificationActionExecutor import io.rebble.cobble.shared.domain.calendar.AndroidCalendarActionExecutor import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.cobble.shared.domain.notifications.CallNotificationProcessor import io.rebble.cobble.shared.domain.notifications.NotificationProcessor import io.rebble.cobble.shared.handlers.* import io.rebble.cobble.shared.handlers.music.MusicHandler @@ -56,4 +57,5 @@ val androidModule = module { } singleOf(::NotificationProcessor) + singleOf(::CallNotificationProcessor) } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt index f203767e..76910179 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt @@ -39,5 +39,14 @@ fun TestPage(onShowSnackbar: (String) -> Unit) { }) { Text("Clear locker") } + + OutlinedButton(onClick = { + watchConnection.watchOrNull?.connectionScope?.value?.launch { + LockerSyncJob.schedule(koin.get()) + onShowSnackbar("Syncing locker") + } + }) { + Text("Sync locker") + } } } \ No newline at end of file From 8e592fa457671ef32ee4f38083c91da5587b9be5 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 24 Sep 2024 17:48:38 +0100 Subject: [PATCH 12/20] locker ui reload button, more logging on error --- .../shared/ui/view/home/locker/Locker.kt | 29 +++++++++++++------ .../shared/ui/viewmodel/LockerViewModel.kt | 15 ++++++++-- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt index 67de94bb..4338e470 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt @@ -41,18 +41,29 @@ fun Locker(page: LockerTabs, lockerDao: LockerDao = getKoin().get(), viewModel: } } - if (entriesState is LockerViewModel.LockerEntriesState.Loaded) { - when (page) { - LockerTabs.Apps -> { - LockerAppList(viewModel, onOpenModalSheet = { viewModel.openModalSheet(it) }) - } + when (entriesState) { + is LockerViewModel.LockerEntriesState.Loaded -> { + when (page) { + LockerTabs.Apps -> { + LockerAppList(viewModel, onOpenModalSheet = { viewModel.openModalSheet(it) }) + } - LockerTabs.Watchfaces -> { - LockerWatchfaceList(viewModel, onOpenModalSheet = { viewModel.openModalSheet(it) }) + LockerTabs.Watchfaces -> { + LockerWatchfaceList(viewModel, onOpenModalSheet = { viewModel.openModalSheet(it) }) + } + } + } + is LockerViewModel.LockerEntriesState.Error -> { + Text("Error loading locker entries") + Button(onClick = { + viewModel.reloadLocker() + }) { + Text("Retry") } } - } else { - CircularProgressIndicator(modifier = Modifier.align(CenterHorizontally)) + is LockerViewModel.LockerEntriesState.Loading -> { + CircularProgressIndicator(modifier = Modifier.align(CenterHorizontally)) + } } } if (modalSheetState is LockerViewModel.ModalSheetState.Open) { diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt index df733a9f..9f2454f7 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt @@ -2,6 +2,7 @@ package io.rebble.cobble.shared.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.database.dao.LockerDao import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms import io.rebble.cobble.shared.domain.state.ConnectionStateManager @@ -13,14 +14,20 @@ import kotlinx.coroutines.sync.withLock class LockerViewModel(private val lockerDao: LockerDao): ViewModel() { open class LockerEntriesState { object Loading : LockerEntriesState() + object Error : LockerEntriesState() data class Loaded(val entries: List) : LockerEntriesState() } open class ModalSheetState { object Closed : ModalSheetState() data class Open(val viewModel: LockerItemViewModel) : ModalSheetState() } - val entriesState = lockerDao.getAllEntriesFlow().map { - LockerEntriesState.Loaded(it) + private val entriesFlow = lockerDao.getAllEntriesFlow() + private val reloadFlow = MutableStateFlow(Unit) + val entriesState: StateFlow = combine(entriesFlow, reloadFlow) { entries, _ -> + LockerEntriesState.Loaded(entries) as LockerEntriesState + }.catch { + Logging.e("Error loading locker entries", it) + emit(LockerEntriesState.Error) }.stateIn(viewModelScope + Dispatchers.IO, SharingStarted.Eagerly, LockerEntriesState.Loading) private var mutex = Mutex() @@ -49,4 +56,8 @@ class LockerViewModel(private val lockerDao: LockerDao): ViewModel() { fun closeModalSheet() { _modalSheetState.value = ModalSheetState.Closed } + + fun reloadLocker() { + reloadFlow.value = Unit + } } \ No newline at end of file From 5fa2dec8cce65142893371de23ca536fd94ed5f0 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 25 Sep 2024 17:22:38 +0100 Subject: [PATCH 13/20] locker ui fix error getting entries(?) --- .../shared/ui/view/home/locker/Locker.kt | 5 ---- .../shared/ui/viewmodel/LockerViewModel.kt | 24 +++++++++---------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt index 4338e470..4fdbb625 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt @@ -55,11 +55,6 @@ fun Locker(page: LockerTabs, lockerDao: LockerDao = getKoin().get(), viewModel: } is LockerViewModel.LockerEntriesState.Error -> { Text("Error loading locker entries") - Button(onClick = { - viewModel.reloadLocker() - }) { - Text("Retry") - } } is LockerViewModel.LockerEntriesState.Loading -> { CircularProgressIndicator(modifier = Modifier.align(CenterHorizontally)) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt index 9f2454f7..a2ce89c0 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt @@ -21,14 +21,18 @@ class LockerViewModel(private val lockerDao: LockerDao): ViewModel() { object Closed : ModalSheetState() data class Open(val viewModel: LockerItemViewModel) : ModalSheetState() } - private val entriesFlow = lockerDao.getAllEntriesFlow() - private val reloadFlow = MutableStateFlow(Unit) - val entriesState: StateFlow = combine(entriesFlow, reloadFlow) { entries, _ -> - LockerEntriesState.Loaded(entries) as LockerEntriesState - }.catch { - Logging.e("Error loading locker entries", it) - emit(LockerEntriesState.Error) - }.stateIn(viewModelScope + Dispatchers.IO, SharingStarted.Eagerly, LockerEntriesState.Loading) + + val entriesState = MutableStateFlow(LockerEntriesState.Loading) + init { + viewModelScope.launch { + lockerDao.getAllEntriesFlow().catch { + Logging.e("Error loading locker entries", it) + entriesState.value = LockerEntriesState.Error + }.collect { entries -> + entriesState.value = LockerEntriesState.Loaded(entries) + } + } + } private var mutex = Mutex() private var lastJob: Job? = null @@ -56,8 +60,4 @@ class LockerViewModel(private val lockerDao: LockerDao): ViewModel() { fun closeModalSheet() { _modalSheetState.value = ModalSheetState.Closed } - - fun reloadLocker() { - reloadFlow.value = Unit - } } \ No newline at end of file From 2ec5c541311f389b6fe5f41c9379613bf139f111 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 25 Sep 2024 18:35:13 +0100 Subject: [PATCH 14/20] dont close db when we aren't truly 'closed' yet --- .../app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt b/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt index 863fef65..72b6506e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt @@ -128,7 +128,7 @@ class FlutterMainActivity : FlutterActivity() { } override fun onDestroy() { - closeDatabase() + //closeDatabase() super.onDestroy() } From cc989e4f10491451dafe71de30105be0954e7b38 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 4 Oct 2024 18:54:16 +0100 Subject: [PATCH 15/20] PKJS finalize appmessage comms, add watch/account token support, move pebblekit android to its own module --- .../cobble/shared/js/WebViewJsRunnerTest.kt | 3 +- .../ui/FirmwareUpdateControlFlutterBridge.kt | 9 +- android/build.gradle.kts | 1 + android/gradle/libs.versions.toml | 13 +- android/pebblekit_android/.gitignore | 1 + android/pebblekit_android/build.gradle.kts | 38 ++ android/pebblekit_android/consumer-rules.pro | 0 android/pebblekit_android/proguard-rules.pro | 21 + .../android/kit/ExampleInstrumentedTest.java | 26 + .../src/main/AndroidManifest.xml | 4 + .../com/getpebble/android/kit/Constants.java | 0 .../java}/com/getpebble/android/kit/LICENSE | 0 .../com/getpebble/android/kit/PebbleKit.java | 0 .../android/kit/util/PebbleDictionary.java | 0 .../android/kit/util/PebbleTuple.java | 0 .../android/kit/util/SportsState.java | 0 .../android/kit/ExampleUnitTest.java | 17 + android/settings.gradle | 1 + android/shared/build.gradle.kts | 4 + .../10.json | 511 ++++++++++++++++++ .../rebble/cobble/shared/di/AndroidModule.kt | 8 +- .../handlers/AndroidPlatformAppMessageIPC.kt | 3 - .../handlers/AppInstallHandler.android.kt | 21 + .../shared/js/JsRunnerFactory.android.kt | 5 +- .../io/rebble/cobble/shared/js/JsTokenUtil.kt | 41 ++ .../cobble/shared/js/WebViewJsRunner.kt | 34 +- .../cobble/shared/js/WebViewPKJSInterface.kt | 14 +- .../shared/js/WebViewPrivatePKJSInterface.kt | 43 +- .../cobble/shared/api/AppstoreClient.kt | 3 +- .../io/rebble/cobble/shared/api/AuthClient.kt | 34 ++ .../kotlin/io/rebble/cobble/shared/api/RWS.kt | 5 + .../shared/data/js/ActivePebbleWatchInfo.kt | 42 ++ .../cobble/shared/database/AppDatabase.kt | 3 +- .../database/entity/SyncedLockerEntry.kt | 1 + .../cobble/shared/di/LibPebbleModule.kt | 1 + .../shared/domain/api/appstore/LockerEntry.kt | 1 + .../shared/domain/api/auth/RWSAccount.kt | 22 + .../AppMessageTransactionSequence.kt | 21 + .../shared/domain/common/PebbleDevice.kt | 9 + .../shared/domain/common/PebbleWatchModel.kt | 46 ++ .../shared/handlers/AppInstallHandler.kt | 23 +- .../shared/handlers/AppMessageHandler.kt | 10 +- .../shared/handlers/AppRunStateHandler.kt | 2 + .../shared/handlers/PKJSLifecycleHandler.kt | 50 ++ .../io/rebble/cobble/shared/js/JsRunner.kt | 11 +- .../cobble/shared/js/JsRunnerFactory.kt | 3 +- .../io/rebble/cobble/shared/js/PKJSApp.kt | 122 ++++- .../cobble/shared/js/PrivatePKJSInterface.kt | 1 + .../shared/ui/viewmodel/LockerViewModel.kt | 2 +- .../util/RunningFirmwareVersionParsed.kt | 23 + .../shared/handlers/AppInstallHandler.ios.kt | 6 + .../cobble/shared/js/JsRunnerFactory.ios.kt | 3 +- 52 files changed, 1200 insertions(+), 62 deletions(-) create mode 100644 android/pebblekit_android/.gitignore create mode 100644 android/pebblekit_android/build.gradle.kts create mode 100644 android/pebblekit_android/consumer-rules.pro create mode 100644 android/pebblekit_android/proguard-rules.pro create mode 100644 android/pebblekit_android/src/androidTest/java/com/getpebble/android/kit/ExampleInstrumentedTest.java create mode 100644 android/pebblekit_android/src/main/AndroidManifest.xml rename android/{shared/src/androidMain/kotlin => pebblekit_android/src/main/java}/com/getpebble/android/kit/Constants.java (100%) rename android/{shared/src/androidMain/kotlin => pebblekit_android/src/main/java}/com/getpebble/android/kit/LICENSE (100%) rename android/{shared/src/androidMain/kotlin => pebblekit_android/src/main/java}/com/getpebble/android/kit/PebbleKit.java (100%) rename android/{shared/src/androidMain/kotlin => pebblekit_android/src/main/java}/com/getpebble/android/kit/util/PebbleDictionary.java (100%) rename android/{shared/src/androidMain/kotlin => pebblekit_android/src/main/java}/com/getpebble/android/kit/util/PebbleTuple.java (100%) rename android/{shared/src/androidMain/kotlin => pebblekit_android/src/main/java}/com/getpebble/android/kit/util/SportsState.java (100%) create mode 100644 android/pebblekit_android/src/test/java/com/getpebble/android/kit/ExampleUnitTest.java create mode 100644 android/shared/schemas/io.rebble.cobble.shared.database.AppDatabase/10.json create mode 100644 android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsTokenUtil.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AuthClient.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/js/ActivePebbleWatchInfo.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/auth/RWSAccount.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/appmessage/AppMessageTransactionSequence.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleWatchModel.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/PKJSLifecycleHandler.kt create mode 100644 android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/RunningFirmwareVersionParsed.kt diff --git a/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt b/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt index 3043b331..0d5774ba 100644 --- a/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt +++ b/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt @@ -2,6 +2,7 @@ package io.rebble.cobble.shared.js import android.content.Context import androidx.test.platform.app.InstrumentationRegistry +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import io.rebble.libpebblecommon.util.runBlocking import kotlinx.coroutines.CoroutineScope @@ -77,7 +78,7 @@ class WebViewJsRunnerTest { """.trimIndent() ) val printTestPath = assetsToSdcard("print_test.js") - val webViewJsRunner = WebViewJsRunner(context, MutableStateFlow("dummy"), coroutineScope, appInfo, printTestPath) + val webViewJsRunner = WebViewJsRunner(context, PebbleDevice(null, "dummy"), coroutineScope, appInfo, printTestPath) webViewJsRunner.start() delay(1000) webViewJsRunner.stop() diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt index d9eff2e8..c3a061ec 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt @@ -17,11 +17,8 @@ import io.rebble.libpebblecommon.packets.SystemMessage import io.rebble.libpebblecommon.packets.TimeMessage import io.rebble.libpebblecommon.services.SystemService import io.rebble.libpebblecommon.util.Crc32Calculator -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import okio.buffer @@ -155,7 +152,9 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( val job = connectionScope.launch { try { updatingDevice.putBytesController.status.collect { - firmwareUpdateCallbacks.onFirmwareUpdateProgress(it.progress) {} + withContext(Dispatchers.Main) { + firmwareUpdateCallbacks.onFirmwareUpdateProgress(it.progress) {} + } } } catch (_: CancellationException) { } diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 086a909b..c4f3ec3d 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -23,6 +23,7 @@ plugins { alias(libs.plugins.ksp) apply false alias(libs.plugins.serialization) apply false alias(libs.plugins.jetbrains.compose) apply false + alias(libs.plugins.jetbrains.kotlinx.atomicfu) apply false } allprojects { diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index f5acdb48..d028a068 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -15,6 +15,7 @@ libpebblecommonVersion = "0.1.24" errorproneVersion = "2.26.1" rruleVersion = "1.0.3" spotbugsVersion = "4.8.6" +atomicfu = "0.25.0" protoliteWellKnownTypes = "18.0.0" room = "2.7.0-alpha08" @@ -31,6 +32,10 @@ reorderable = "2.3.3" appcompat = "1.7.0" ktorVersion = "2.3.12" workManagerVersion = "2.9.0" +junit = "4.13.2" +junitVersion = "1.2.1" +espressoCore = "3.6.1" +material = "1.10.0" [plugins] multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } @@ -44,6 +49,7 @@ android-library = { id = "com.android.library", version.ref = "gradle" } android-application = { id = "com.android.application", version.ref = "gradle" } jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-lib" } jetbrains-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +jetbrains-kotlinx-atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" } [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } @@ -80,6 +86,7 @@ libpebblecommon = { module = "io.rebble.libpebblecommon:libpebblecommon", versio timber = { module = "com.jakewharton.timber:timber", version.ref = "timberVersion" } uuid = { module = "com.benasher44:uuid", version.ref = "uuidVersion" } protolite-wellknowntypes = { module = "com.google.firebase:protolite-well-known-types", version.ref = "protoliteWellKnownTypes" } +jetbrains-kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorVersion" } ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktorVersion" } @@ -92,4 +99,8 @@ compose-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewm compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-lib" } compose-components-reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } \ No newline at end of file +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } \ No newline at end of file diff --git a/android/pebblekit_android/.gitignore b/android/pebblekit_android/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/android/pebblekit_android/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/pebblekit_android/build.gradle.kts b/android/pebblekit_android/build.gradle.kts new file mode 100644 index 00000000..b72afc06 --- /dev/null +++ b/android/pebblekit_android/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "com.getpebble.android.kit" + compileSdk = 34 + + defaultConfig { + minSdk = 29 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/android/pebblekit_android/consumer-rules.pro b/android/pebblekit_android/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/android/pebblekit_android/proguard-rules.pro b/android/pebblekit_android/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/android/pebblekit_android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/pebblekit_android/src/androidTest/java/com/getpebble/android/kit/ExampleInstrumentedTest.java b/android/pebblekit_android/src/androidTest/java/com/getpebble/android/kit/ExampleInstrumentedTest.java new file mode 100644 index 00000000..4d5e4c81 --- /dev/null +++ b/android/pebblekit_android/src/androidTest/java/com/getpebble/android/kit/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getpebble.android.kit; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.getpebble.android.kit.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/android/pebblekit_android/src/main/AndroidManifest.xml b/android/pebblekit_android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/android/pebblekit_android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/Constants.java b/android/pebblekit_android/src/main/java/com/getpebble/android/kit/Constants.java similarity index 100% rename from android/shared/src/androidMain/kotlin/com/getpebble/android/kit/Constants.java rename to android/pebblekit_android/src/main/java/com/getpebble/android/kit/Constants.java diff --git a/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/LICENSE b/android/pebblekit_android/src/main/java/com/getpebble/android/kit/LICENSE similarity index 100% rename from android/shared/src/androidMain/kotlin/com/getpebble/android/kit/LICENSE rename to android/pebblekit_android/src/main/java/com/getpebble/android/kit/LICENSE diff --git a/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/PebbleKit.java b/android/pebblekit_android/src/main/java/com/getpebble/android/kit/PebbleKit.java similarity index 100% rename from android/shared/src/androidMain/kotlin/com/getpebble/android/kit/PebbleKit.java rename to android/pebblekit_android/src/main/java/com/getpebble/android/kit/PebbleKit.java diff --git a/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/PebbleDictionary.java b/android/pebblekit_android/src/main/java/com/getpebble/android/kit/util/PebbleDictionary.java similarity index 100% rename from android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/PebbleDictionary.java rename to android/pebblekit_android/src/main/java/com/getpebble/android/kit/util/PebbleDictionary.java diff --git a/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/PebbleTuple.java b/android/pebblekit_android/src/main/java/com/getpebble/android/kit/util/PebbleTuple.java similarity index 100% rename from android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/PebbleTuple.java rename to android/pebblekit_android/src/main/java/com/getpebble/android/kit/util/PebbleTuple.java diff --git a/android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/SportsState.java b/android/pebblekit_android/src/main/java/com/getpebble/android/kit/util/SportsState.java similarity index 100% rename from android/shared/src/androidMain/kotlin/com/getpebble/android/kit/util/SportsState.java rename to android/pebblekit_android/src/main/java/com/getpebble/android/kit/util/SportsState.java diff --git a/android/pebblekit_android/src/test/java/com/getpebble/android/kit/ExampleUnitTest.java b/android/pebblekit_android/src/test/java/com/getpebble/android/kit/ExampleUnitTest.java new file mode 100644 index 00000000..98ffd6ba --- /dev/null +++ b/android/pebblekit_android/src/test/java/com/getpebble/android/kit/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.getpebble.android.kit; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index 2f625af9..0b38d19d 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -23,3 +23,4 @@ plugins.each { name, path -> include ':pebble_bt_transport' include ':shared' +include ':pebblekit_android' diff --git a/android/shared/build.gradle.kts b/android/shared/build.gradle.kts index c14ab7b4..df3f02e9 100644 --- a/android/shared/build.gradle.kts +++ b/android/shared/build.gradle.kts @@ -9,6 +9,7 @@ plugins { alias(libs.plugins.serialization) alias(libs.plugins.jetbrains.compose) alias(libs.plugins.jetbrains.compose.compiler) + alias(libs.plugins.jetbrains.kotlinx.atomicfu) } val timberVersion = "5.0.1" @@ -47,6 +48,8 @@ kotlin { api(libs.koin.core) api(libs.kotlinx.serialization.core) + //XXX: Workaround for https://github.com/Kotlin/kotlinx-atomicfu/issues/469 + implementation(libs.jetbrains.kotlinx.atomicfu) implementation(libs.koin.compose) implementation(libs.uuid) implementation(libs.kotlinx.serialization.json) @@ -76,6 +79,7 @@ kotlin { implementation(libs.androidx.core.ktx) implementation(libs.timber) implementation(libs.rrule) + implementation(project(":pebblekit_android")) } commonTest.dependencies { implementation(kotlin("test")) diff --git a/android/shared/schemas/io.rebble.cobble.shared.database.AppDatabase/10.json b/android/shared/schemas/io.rebble.cobble.shared.database.AppDatabase/10.json new file mode 100644 index 00000000..ff0b8114 --- /dev/null +++ b/android/shared/schemas/io.rebble.cobble.shared.database.AppDatabase/10.json @@ -0,0 +1,511 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "00caae9da897ae6cb913bf321f2135ee", + "entities": [ + { + "tableName": "Calendar", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `platformId` TEXT NOT NULL, `name` TEXT NOT NULL, `ownerName` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `color` INTEGER NOT NULL, `enabled` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "platformId", + "columnName": "platformId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerName", + "columnName": "ownerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Calendar_platformId", + "unique": true, + "columnNames": [ + "platformId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Calendar_platformId` ON `${TABLE_NAME}` (`platformId`)" + } + ] + }, + { + "tableName": "TimelinePin", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `parentId` TEXT NOT NULL, `backingId` TEXT, `timestamp` INTEGER NOT NULL, `duration` INTEGER, `type` TEXT NOT NULL, `isVisible` INTEGER NOT NULL, `isFloating` INTEGER NOT NULL, `isAllDay` INTEGER NOT NULL, `persistQuickView` INTEGER NOT NULL, `layout` TEXT NOT NULL, `attributesJson` TEXT, `actionsJson` TEXT, `nextSyncAction` TEXT, PRIMARY KEY(`itemId`))", + "fields": [ + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backingId", + "columnName": "backingId", + "affinity": "TEXT" + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVisible", + "columnName": "isVisible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFloating", + "columnName": "isFloating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "persistQuickView", + "columnName": "persistQuickView", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "layout", + "columnName": "layout", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributesJson", + "columnName": "attributesJson", + "affinity": "TEXT" + }, + { + "fieldPath": "actionsJson", + "columnName": "actionsJson", + "affinity": "TEXT" + }, + { + "fieldPath": "nextSyncAction", + "columnName": "nextSyncAction", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "itemId" + ] + } + }, + { + "tableName": "PersistedNotification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sbnKey` TEXT NOT NULL, `packageName` TEXT NOT NULL, `postTime` INTEGER NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL, `groupKey` TEXT, PRIMARY KEY(`sbnKey`))", + "fields": [ + { + "fieldPath": "sbnKey", + "columnName": "sbnKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postTime", + "columnName": "postTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "groupKey", + "columnName": "groupKey", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sbnKey" + ] + } + }, + { + "tableName": "CachedPackageInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `flags` INTEGER NOT NULL, `updated` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updated", + "columnName": "updated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "NotificationChannel", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageId` TEXT NOT NULL, `channelId` TEXT NOT NULL, `name` TEXT, `description` TEXT, `conversationId` TEXT, `shouldNotify` INTEGER NOT NULL, PRIMARY KEY(`packageId`, `channelId`))", + "fields": [ + { + "fieldPath": "packageId", + "columnName": "packageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "conversationId", + "columnName": "conversationId", + "affinity": "TEXT" + }, + { + "fieldPath": "shouldNotify", + "columnName": "shouldNotify", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageId", + "channelId" + ] + } + }, + { + "tableName": "SyncedLockerEntry", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `uuid` TEXT NOT NULL, `version` TEXT NOT NULL, `title` TEXT NOT NULL, `type` TEXT NOT NULL, `hearts` INTEGER NOT NULL, `developerName` TEXT NOT NULL, `developerId` TEXT, `configurable` INTEGER NOT NULL, `timelineEnabled` INTEGER NOT NULL, `removeLink` TEXT NOT NULL, `shareLink` TEXT NOT NULL, `pbwLink` TEXT NOT NULL, `pbwReleaseId` TEXT NOT NULL, `pbwIconResourceId` INTEGER NOT NULL DEFAULT 0, `nextSyncAction` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hearts", + "columnName": "hearts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "developerName", + "columnName": "developerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "developerId", + "columnName": "developerId", + "affinity": "TEXT" + }, + { + "fieldPath": "configurable", + "columnName": "configurable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timelineEnabled", + "columnName": "timelineEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removeLink", + "columnName": "removeLink", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareLink", + "columnName": "shareLink", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pbwLink", + "columnName": "pbwLink", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pbwReleaseId", + "columnName": "pbwReleaseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pbwIconResourceId", + "columnName": "pbwIconResourceId", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nextSyncAction", + "columnName": "nextSyncAction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SyncedLockerEntry_uuid", + "unique": true, + "columnNames": [ + "uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SyncedLockerEntry_uuid` ON `${TABLE_NAME}` (`uuid`)" + } + ] + }, + { + "tableName": "SyncedLockerEntryPlatform", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`platformEntryId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `lockerEntryId` TEXT NOT NULL, `sdkVersion` TEXT NOT NULL, `processInfoFlags` INTEGER NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `icon` TEXT, `list` TEXT, `screenshot` TEXT, FOREIGN KEY(`lockerEntryId`) REFERENCES `SyncedLockerEntry`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "platformEntryId", + "columnName": "platformEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockerEntryId", + "columnName": "lockerEntryId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sdkVersion", + "columnName": "sdkVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "processInfoFlags", + "columnName": "processInfoFlags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images.icon", + "columnName": "icon", + "affinity": "TEXT" + }, + { + "fieldPath": "images.list", + "columnName": "list", + "affinity": "TEXT" + }, + { + "fieldPath": "images.screenshot", + "columnName": "screenshot", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "platformEntryId" + ] + }, + "indices": [ + { + "name": "index_SyncedLockerEntryPlatform_lockerEntryId", + "unique": false, + "columnNames": [ + "lockerEntryId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SyncedLockerEntryPlatform_lockerEntryId` ON `${TABLE_NAME}` (`lockerEntryId`)" + } + ], + "foreignKeys": [ + { + "table": "SyncedLockerEntry", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockerEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '00caae9da897ae6cb913bf321f2135ee')" + ] + } +} \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt index f0a0408f..f7d030d3 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt @@ -29,6 +29,9 @@ val androidModule = module { AndroidPlatformContext(androidContext().applicationContext) } bind PlatformContext::class single { createDataStore(androidContext()) } + factory { + androidContext().packageManager + } single(named("activeNotifsState")) { MutableStateFlow>(emptyMap()) @@ -45,7 +48,9 @@ val androidModule = module { AppInstallHandler(pebbleDevice), CalendarActionHandler(pebbleDevice), CalendarHandler(pebbleDevice), - MusicHandler(pebbleDevice) + MusicHandler(pebbleDevice), + PKJSLifecycleHandler(pebbleDevice), + AppMessageHandler(pebbleDevice), ) } @@ -58,4 +63,5 @@ val androidModule = module { singleOf(::NotificationProcessor) singleOf(::CallNotificationProcessor) + singleOf(::AndroidPlatformAppMessageIPC) bind PlatformAppMessageIPC::class } \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AndroidPlatformAppMessageIPC.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AndroidPlatformAppMessageIPC.kt index f5807397..3f0212bf 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AndroidPlatformAppMessageIPC.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AndroidPlatformAppMessageIPC.kt @@ -10,12 +10,9 @@ import io.rebble.cobble.shared.util.coroutines.asFlow import io.rebble.cobble.shared.util.getIntExtraOrNull import io.rebble.cobble.shared.util.getPebbleDictionary import io.rebble.cobble.shared.util.toPacket -import io.rebble.libpebblecommon.packets.AppCustomizationSetStockAppTitleMessage import io.rebble.libpebblecommon.packets.AppMessage import io.rebble.libpebblecommon.packets.AppType -import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull class AndroidPlatformAppMessageIPC(private val context: Context): PlatformAppMessageIPC { diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.android.kt index 8a83a071..8d15dcae 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.android.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.android.kt @@ -1,11 +1,16 @@ package io.rebble.cobble.shared.handlers +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsChannel import io.ktor.util.cio.use import io.ktor.util.cio.writeChannel import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.copyAndClose import io.rebble.cobble.shared.AndroidPlatformContext +import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.PlatformContext +import io.rebble.cobble.shared.database.dao.LockerDao import io.rebble.cobble.shared.util.File import io.rebble.cobble.shared.util.toKMPFile @@ -23,4 +28,20 @@ actual suspend fun savePbwFile(context: PlatformContext, appUuid: String, byteRe byteReadChannel.copyAndClose(this) } return file.file.toURI().toString() +} + +actual suspend fun downloadPbw(context: PlatformContext, httpClient: HttpClient, lockerDao: LockerDao, appUuid: String): String? { + val row = lockerDao.getEntryByUuid(appUuid) + val url = row?.entry?.pbwLink ?: run { + Logging.e("App URL for $appUuid not found in locker") + return null + } + + val response = httpClient.get(url) + if (response.status.value != 200) { + Logging.e("Failed to download app $appUuid: ${response.status}") + return null + } else { + return savePbwFile(context, appUuid, response.bodyAsChannel()) + } } \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt index 30d7653b..df2cc55a 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt @@ -1,6 +1,7 @@ package io.rebble.cobble.shared.js import android.content.Context +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow @@ -11,8 +12,8 @@ actual object JsRunnerFactory: KoinComponent { private val context: Context by inject() actual fun createJsRunner( scope: CoroutineScope, - connectedAddress: String, + device: PebbleDevice, appInfo: PbwAppInfo, jsPath: String - ): JsRunner = WebViewJsRunner(context, connectedAddress, scope, appInfo, jsPath) + ): JsRunner = WebViewJsRunner(context, device, scope, appInfo, jsPath) } \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsTokenUtil.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsTokenUtil.kt new file mode 100644 index 00000000..cc4aeabb --- /dev/null +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsTokenUtil.kt @@ -0,0 +1,41 @@ +package io.rebble.cobble.shared.js + +import com.benasher44.uuid.Uuid +import io.rebble.cobble.shared.api.RWS +import io.rebble.cobble.shared.database.dao.LockerDao +import io.rebble.cobble.shared.domain.common.PebbleDevice +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.security.MessageDigest +import java.util.Locale + +object JsTokenUtil: KoinComponent { + private val lockerDao: LockerDao by inject() + private const val ACCOUNT_TOKEN_SALT = "MMIxeUT[G9/U#(7V67O^EuADSw,{\$C;B}`>|- lrQCs|t|k=P_!*LETm,RKc,BG*'" + + private fun md5Digest(input: String): String { + val digest = MessageDigest.getInstance("md5") + digest.update(input.toByteArray()) + val bytes = digest.digest() + return bytes.joinToString(separator = "") { String.format("%02X", it) }.lowercase(Locale.US) + } + + private suspend fun generateToken(uuid: Uuid, seed: String): String { + val developerId = lockerDao.getEntryByUuid(uuid.toString())?.entry?.developerId + val unhashed = buildString { + append(seed) + append(developerId ?: uuid.toString().uppercase(Locale.US)) + append(ACCOUNT_TOKEN_SALT) + } + return md5Digest(unhashed) + } + + suspend fun getWatchToken(uuid: Uuid, device: PebbleDevice): String { + val serial = device.metadata.value?.serial?.get() ?: throw IllegalArgumentException("Device has no serial") + return generateToken(uuid, serial) + } + + suspend fun getAccountToken(uuid: Uuid): String? { + return RWS.authClient?.getCurrentAccount()?.uid?.toString()?.let { generateToken(uuid, it) } + } +} \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt index e1f47c90..1f5f2c1f 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt @@ -9,16 +9,18 @@ import android.os.Message import android.view.View import android.webkit.* import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.withContext -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -class WebViewJsRunner(val context: Context, private val connectedAddress: String, private val scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner(appInfo, jsPath) { + +class WebViewJsRunner(val context: Context, device: PebbleDevice, private val scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner(appInfo, jsPath, device) { companion object { const val API_NAMESPACE = "Pebble" @@ -29,7 +31,7 @@ class WebViewJsRunner(val context: Context, private val connectedAddress: String private var webView: WebView? = null private val initializedLock = Object() private val publicJsInterface = WebViewPKJSInterface(this) - private val privateJsInterface = WebViewPrivatePKJSInterface(this, scope) + private val privateJsInterface = WebViewPrivatePKJSInterface(this, scope, _outgoingAppMessages) private val interfaces = setOf( Pair(API_NAMESPACE, publicJsInterface), Pair(PRIVATE_API_NAMESPACE, privateJsInterface) @@ -178,6 +180,7 @@ class WebViewJsRunner(val context: Context, private val connectedAddress: String throw e } check(webView != null) { "WebView not initialized" } + Logging.d("WebView initialized") loadApp(jsPath) } @@ -238,10 +241,31 @@ class WebViewJsRunner(val context: Context, private val connectedAddress: String } suspend fun signalReady() { - val readyDeviceIds = listOf(connectedAddress) + val readyDeviceIds = listOf(device.address) val readyJson = Json.encodeToString(readyDeviceIds) withContext(Dispatchers.Main) { webView?.loadUrl("javascript:signalReady(${Uri.encode(readyJson)})") } } + + override suspend fun signalNewAppMessageData(data: String?): Boolean { + withContext(Dispatchers.Main) { + webView?.loadUrl("javascript:signalNewAppMessageData(${Uri.encode("'" + (data ?: "null") + "'")})") + } + return true + } + + override suspend fun signalAppMessageAck(data: String?): Boolean { + withContext(Dispatchers.Main) { + webView?.loadUrl("javascript:signalAppMessageAck(${Uri.encode("'" + (data ?: "null") + "'")})") + } + return true + } + + override suspend fun signalAppMessageNack(data: String?): Boolean { + withContext(Dispatchers.Main) { + webView?.loadUrl("javascript:signalAppMessageNack(${Uri.encode("'" + (data ?: "null") + "'")})") + } + return true + } } \ No newline at end of file diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt index a5864934..443c718b 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPKJSInterface.kt @@ -3,7 +3,10 @@ package io.rebble.cobble.shared.js import android.content.Context import android.webkit.JavascriptInterface import android.widget.Toast +import com.benasher44.uuid.uuidFrom import io.rebble.cobble.shared.Logging +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -17,14 +20,17 @@ class WebViewPKJSInterface(private val jsRunner: JsRunner): PKJSInterface, KoinC @JavascriptInterface override fun getAccountToken(): String { - //TODO - return "" + //XXX: This is a blocking call, but it's fine because it's called from a WebView thread, maybe + return runBlocking(Dispatchers.IO) { + JsTokenUtil.getAccountToken(uuidFrom(jsRunner.appInfo.uuid)) ?: "" + } } @JavascriptInterface override fun getWatchToken(): String { - //TODO - return "" + return runBlocking(Dispatchers.IO) { + JsTokenUtil.getWatchToken(uuidFrom(jsRunner.appInfo.uuid), jsRunner.device) + } } @JavascriptInterface diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt index 80d317e6..622b9cf7 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewPrivatePKJSInterface.kt @@ -3,34 +3,47 @@ package io.rebble.cobble.shared.js import android.net.Uri import android.webkit.JavascriptInterface import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.data.js.ActivePebbleWatchInfo +import io.rebble.cobble.shared.data.js.fromDevice +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json -class WebViewPrivatePKJSInterface(private val jsRunner: WebViewJsRunner, private val scope: CoroutineScope): PrivatePKJSInterface { +class WebViewPrivatePKJSInterface(private val jsRunner: WebViewJsRunner, private val scope: CoroutineScope, private val outgoingAppMessages: MutableSharedFlow): PrivatePKJSInterface { @JavascriptInterface override fun privateLog(message: String) { - TODO("Not yet implemented") + Logging.v("privateLog: $message") } @JavascriptInterface override fun logInterceptedSend() { - TODO("Not yet implemented") + Logging.v("logInterceptedSend") } @JavascriptInterface override fun logInterceptedRequest() { - TODO("Not yet implemented") + Logging.v("logInterceptedRequest") } @JavascriptInterface override fun getVersionCode(): Int { + Logging.v("getVersionCode") TODO("Not yet implemented") } + @JavascriptInterface + override fun logLocationRequest() { + Logging.v("logLocationRequest") + } + @JavascriptInterface fun startupScriptHasLoaded(url: String) { - Logging.d("Startup script has loaded: $url") + Logging.v("Startup script has loaded: $url") val uri = Uri.parse(url) val params = uri.getQueryParameter("params") scope.launch { @@ -40,28 +53,48 @@ class WebViewPrivatePKJSInterface(private val jsRunner: WebViewJsRunner, private @JavascriptInterface fun privateFnLocalStorageWrite(key: String, value: String) { + Logging.v("privateFnLocalStorageWrite") TODO("Not yet implemented") } @JavascriptInterface fun privateFnLocalStorageRead(key: String): String { + Logging.v("privateFnLocalStorageRead") TODO("Not yet implemented") } @JavascriptInterface fun privateFnLocalStorageReadAll(): String { + Logging.v("privateFnLocalStorageReadAll") return "{}" } @JavascriptInterface fun privateFnLocalStorageReadAll_AtPreregistrationStage(baseUriReference: String): String { + Logging.v("privateFnLocalStorageReadAll_AtPreregistrationStage") return privateFnLocalStorageReadAll() } @JavascriptInterface fun signalAppScriptLoadedByBootstrap() { + Logging.v("signalAppScriptLoadedByBootstrap") scope.launch { jsRunner.signalReady() } } + + @JavascriptInterface + fun sendAppMessageString(jsonAppMessage: String) { + Logging.v("sendAppMessageString") + if (!outgoingAppMessages.tryEmit(jsonAppMessage)) { + Logging.e("Failed to emit outgoing AppMessage") + } + } + + @JavascriptInterface + fun getActivePebbleWatchInfo(): String { + return ConnectionStateManager.connectionState.value.watchOrNull?.let { + Json.encodeToString(ActivePebbleWatchInfo.fromDevice(it)) + } ?: error("No active watch") + } } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AppstoreClient.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AppstoreClient.kt index 5e5fc4db..26e39563 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AppstoreClient.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AppstoreClient.kt @@ -13,8 +13,7 @@ import org.koin.core.component.inject class AppstoreClient( val baseUrl: String, - private val token: String, - engine: HttpClientEngine? = null, + private val token: String ): KoinComponent { private val version = "v1" private val client: HttpClient by inject() diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AuthClient.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AuthClient.kt new file mode 100644 index 00000000..e08ae7cc --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AuthClient.kt @@ -0,0 +1,34 @@ +package io.rebble.cobble.shared.api + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.request.get +import io.ktor.client.request.headers +import io.ktor.http.HttpHeaders +import io.rebble.cobble.shared.domain.api.auth.RWSAccount +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class AuthClient( + val baseUrl: String, + private val token: String +): KoinComponent { + private val version = "v1" + private val client: HttpClient by inject() + + suspend fun getCurrentAccount(): RWSAccount { + val res = client.get("$baseUrl/$version/me") { + headers { + append(HttpHeaders.Accept, "application/json") + append(HttpHeaders.Authorization, "Bearer $token") + } + } + + if (res.status.value != 200) { + error("Failed to get account: ${res.status}") + } + + return res.body() ?: error("Failed to deserialize account") + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/RWS.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/RWS.kt index fdf33d0e..b90b0004 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/RWS.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/RWS.kt @@ -17,6 +17,11 @@ object RWS: KoinComponent { private val _appstoreClient = token.map { it.tokenOrNull?.let { t -> AppstoreClient("https://appstore-api.$domainSuffix/api", t) } }.stateIn(scope, SharingStarted.Eagerly, null) + private val _authClient = token.map { + it.tokenOrNull?.let { t -> AuthClient("https://auth.$domainSuffix/api", t) } + }.stateIn(scope, SharingStarted.Eagerly, null) val appstoreClient: AppstoreClient? get() = _appstoreClient.value + val authClient: AuthClient? + get() = _authClient.value } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/js/ActivePebbleWatchInfo.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/js/ActivePebbleWatchInfo.kt new file mode 100644 index 00000000..4951b8bc --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/js/ActivePebbleWatchInfo.kt @@ -0,0 +1,42 @@ +package io.rebble.cobble.shared.data.js + +import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.cobble.shared.domain.common.PebbleWatchModel +import io.rebble.cobble.shared.util.parsed +import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform +import kotlinx.serialization.Serializable + +@Serializable +data class ActivePebbleWatchInfo( + val platform: String, + val model: String, + val language: String, + val firmware: FirmwareVersion +) { + @Serializable + data class FirmwareVersion( + val major: Int, + val minor: Int, + val patch: Int, + val suffix: String + ) +} + +fun ActivePebbleWatchInfo.Companion.fromDevice(device: PebbleDevice): ActivePebbleWatchInfo { + val metadata = device.metadata.value ?: error("Device metadata is null") + val modelId = device.modelId.value ?: -1 + val platform = WatchHardwarePlatform.fromProtocolNumber(metadata.running.hardwarePlatform.get()) + val color = PebbleWatchModel.fromProtocolNumber(modelId) + val parsedFwVersion = metadata.running.parsed() + return ActivePebbleWatchInfo( + platform = platform?.watchType?.codename ?: "unknown", + model = color.jsName, + language = metadata.language.get(), + firmware = ActivePebbleWatchInfo.FirmwareVersion( + major = parsedFwVersion.major, + minor = parsedFwVersion.minor, + patch = parsedFwVersion.patch, + suffix = parsedFwVersion.suffix + ) + ) +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt index 5dcd6b4a..02f3a5b9 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt @@ -22,7 +22,7 @@ import org.koin.mp.KoinPlatformTools SyncedLockerEntry::class, SyncedLockerEntryPlatform::class ], - version = 9, + version = 10, autoMigrations = [ AutoMigration(1, 2), AutoMigration(2, 3), @@ -32,6 +32,7 @@ import org.koin.mp.KoinPlatformTools AutoMigration(6, 7), AutoMigration(7, 8), AutoMigration(8, 9), + AutoMigration(9, 10) ] ) @TypeConverters(Converters::class) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt index 377760cf..d8853bdc 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt @@ -17,6 +17,7 @@ data class SyncedLockerEntry( val type: String, val hearts: Int, val developerName: String, + val developerId: String?, val configurable: Boolean, val timelineEnabled: Boolean, val removeLink: String, diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt index e621c6bc..2d18cbc4 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt @@ -1,5 +1,6 @@ package io.rebble.cobble.shared.di +import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.middleware.PutBytesController import io.rebble.libpebblecommon.ProtocolHandlerImpl diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt index 9d3ad01b..0a9693d0 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt @@ -113,6 +113,7 @@ fun LockerEntry.toEntity(): SyncedLockerEntry { title = title, type = type, hearts = hearts, + developerId = developer.id, developerName = developer.name, configurable = isConfigurable, timelineEnabled = isTimelineEnabled, diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/auth/RWSAccount.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/auth/RWSAccount.kt new file mode 100644 index 00000000..30bca1f5 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/auth/RWSAccount.kt @@ -0,0 +1,22 @@ +package io.rebble.cobble.shared.domain.api.auth + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +@Serializable +data class RWSAccount( + val uid: Long, + val name: String, + @SerialName("is_subscribed") + val isSubscribed: Boolean, + val scopes: List, + @SerialName("is_wizard") + val isWizard: Boolean, + @SerialName("has_timeline") + val hasTimeline: Boolean, + @SerialName("timeline_ttl") + val timelineTtl: Int, + @SerialName("boot_overrides") + val bootOverrides: JsonObject?, +) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/appmessage/AppMessageTransactionSequence.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/appmessage/AppMessageTransactionSequence.kt new file mode 100644 index 00000000..3ca66332 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/appmessage/AppMessageTransactionSequence.kt @@ -0,0 +1,21 @@ +package io.rebble.cobble.shared.domain.appmessage + +import kotlinx.atomicfu.atomic + +class AppMessageTransactionSequence: Sequence { + private val sequence = atomic(0) + + override fun iterator(): Iterator { + if (sequence.value != 0) { + error("Sequence can only be iterated once") + } + return object : Iterator { + override fun hasNext(): Boolean = true + override fun next(): UByte { + sequence.compareAndSet(0x100, 0) + return (sequence.getAndIncrement() and 0xff).toUByte() + } + } + } + +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt index 8f1bfaab..ad0357f9 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt @@ -3,12 +3,15 @@ package io.rebble.cobble.shared.domain.common import com.benasher44.uuid.Uuid import io.ktor.http.parametersOf import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.domain.appmessage.AppMessageTransactionSequence import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.cobble.shared.handlers.CobbleHandler +import io.rebble.cobble.shared.handlers.OutgoingMessage import io.rebble.cobble.shared.middleware.PutBytesController import io.rebble.libpebblecommon.ProtocolHandler +import io.rebble.libpebblecommon.packets.AppMessage import io.rebble.libpebblecommon.packets.WatchVersion import io.rebble.libpebblecommon.services.* import io.rebble.libpebblecommon.services.app.AppRunStateService @@ -16,6 +19,7 @@ import io.rebble.libpebblecommon.services.appmessage.AppMessageService import io.rebble.libpebblecommon.services.blobdb.BlobDBService import io.rebble.libpebblecommon.services.blobdb.TimelineService import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first @@ -38,6 +42,11 @@ open class PebbleDevice( val connectionScope: MutableStateFlow = MutableStateFlow(null) val currentActiveApp: MutableStateFlow = MutableStateFlow(null) + val incomingAppMessages: MutableSharedFlow = MutableSharedFlow(0, 8) + val outgoingAppMessages: MutableSharedFlow = MutableSharedFlow(0, 8) + + val appMessageTransactionSequence = AppMessageTransactionSequence().iterator() + init { // This will init all the handlers by reading the lazy value causing them to be injected negotiationScope.launch { diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleWatchModel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleWatchModel.kt new file mode 100644 index 00000000..4339fe99 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleWatchModel.kt @@ -0,0 +1,46 @@ +package io.rebble.cobble.shared.domain.common + +enum class PebbleWatchModel(val protocolNumber: Int, val jsName: String) { + ClassicBlack(1, "pebble_black"), + ClassicWhite(2, "pebble_white"), + ClassicRed(3, "pebble_red"), + ClassicOrange(4, "pebble_orange"), + ClassicPink(5, "pebble_pink"), + ClassicSteelSilver(6, "pebble_steel_silver"), + ClassicSteelGunmetal(7, "pebble_steel_gunmetal"), + ClassicFlyBlue(8, "pebble_fly_blue"), + ClassicFreshGreen(9, "pebble_fresh_green"), + ClassicHotPink(10, "pebble_hot_pink"), + TimeWhite(11, "pebble_time_white"), + TimeBlack(12, "pebble_time_black"), + TimeRed(13, "pebble_time_red"), + TimeSteelSilver(14, "pebble_time_steel_silver"), + TimeSteelGunmetal(15, "pebble_time_steel_black"), + TimeSteelGold(16, "pebble_time_steel_gold"), + TimeRoundSilver14(17, "pebble_time_round_silver"), + TimeRoundBlack14(18, "pebble_time_round_black"), + TimeRoundSilver20(19, "pebble_time_round_silver_20"), + TimeRoundBlack20(20, "pebble_time_round_black_20"), + TimeRoundRoseGold14(21, "pebble_time_round_rose_gold"), + TimeRoundRainbowSilver14(22, "pebble_time_round_silver_rainbow"), + TimeRoundRainbowBlack20(23, "pebble_time_round_black_rainbow"), + Pebble2SEBlack(24, "pebble_2_se_black_charcoal"), + Pebble2HRBlack(25, "pebble_2_hr_black_charcoal"), + Pebble2SEWhite(26, "pebble_2_se_white_gray"), + Pebble2HRLime(27, "pebble_2_hr_charcoal_sorbet_green"), + Pebble2HRFlame(28, "pebble_2_hr_charcoal_red"), + Pebble2HRWhite(29, "pebble_2_hr_white_gray"), + Pebble2HRAqua(30, "pebble_2_hr_white_turquoise"), + Time2Gunmetal(31, "pebble_time_2_black"), + Time2Silver(32, "pebble_time_2_silver"), + Time2Gold(33, "pebble_time_2_gold"), + TimeRoundBlackSilverPolish20(34, "pebble_time_round_polished_silver"), + TimeRoundBlackGoldPolish20(35, "pebble_time_round_polished_gold"), + Unknown(-1, "unknown_unknown"); + + companion object { + fun fromProtocolNumber(protocolNumber: Int): PebbleWatchModel { + return entries.firstOrNull { it.protocolNumber == protocolNumber } ?: Unknown + } + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt index fe2bce12..9d0ab5b0 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt @@ -52,31 +52,13 @@ class AppInstallHandler( } } - private suspend fun downloadPbw(appUuid: String): String? { - val row = lockerDao.getEntryByUuid(appUuid) - val url = row?.entry?.pbwLink ?: run { - Logging.e("App URL for $appUuid not found in locker") - return null - } - - val response = httpClient.get(url) - if (response.status.value != 200) { - Logging.e("Failed to download app $appUuid: ${response.status}") - return null - } else { - return savePbwFile(platformContext, appUuid, response.bodyAsChannel()) - } - } - - - private suspend fun onNewAppFetchRequest(message: AppFetchRequest) { try { val appUuid = message.uuid.get().toString() var appFile = getAppPbwFile(platformContext, appUuid) if (!appFile.exists()) { - val uri = downloadPbw(appUuid) + val uri = downloadPbw(platformContext, httpClient, lockerDao, appUuid) if (uri == null) { Logging.e("Failed to download app $appUuid") respondFetchRequest(AppFetchResponseStatus.NO_DATA) @@ -146,4 +128,5 @@ class AppInstallHandler( } } expect fun getAppPbwFile(context: PlatformContext, appUuid: String): File -expect suspend fun savePbwFile(context: PlatformContext, appUuid: String, byteReadChannel: ByteReadChannel): String \ No newline at end of file +expect suspend fun savePbwFile(context: PlatformContext, appUuid: String, byteReadChannel: ByteReadChannel): String +expect suspend fun downloadPbw(context: PlatformContext, httpClient: HttpClient, lockerDao: LockerDao, appUuid: String): String? \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt index ff070e20..7e9aedd7 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt @@ -68,7 +68,7 @@ class AppMessageHandler( ) : KoinComponent, CobbleHandler { private val platformAppMessageIPC: PlatformAppMessageIPC by inject() private var lastReceivedMessage: AppMessageTimestamp? = null - private val outgoingMessages = platformAppMessageIPC.outgoingMessages().buffer() + private val outgoingMessages = platformAppMessageIPC.outgoingMessages() init { pebbleDevice.negotiationScope.launch { @@ -85,6 +85,9 @@ class AppMessageHandler( private fun listenForIncomingPackets(deviceScope: CoroutineScope) { deviceScope.launch { for (message in pebbleDevice.appMessageService.receivedMessages) { + if (!pebbleDevice.incomingAppMessages.tryEmit(message)) { + Logging.w("Failed to emit incoming AppMessage") + } when (message) { is AppMessage.AppMessagePush -> { lastReceivedMessage = AppMessageTimestamp(message.uuid.get(), Clock.System.now().toEpochMilliseconds()) @@ -104,7 +107,7 @@ class AppMessageHandler( } private fun listenForOutgoingMessages(deviceScope: CoroutineScope) { - outgoingMessages.buffer().onEach { + pebbleDevice.outgoingAppMessages.onEach { when (it) { is OutgoingMessage.Data -> { if (!isAppActive(it.uuid)) { @@ -131,6 +134,9 @@ class AppMessageHandler( } } }.launchIn(deviceScope) + outgoingMessages.onEach { + pebbleDevice.outgoingAppMessages.emit(it) + }.launchIn(deviceScope) } private fun sendConnectDisconnectEvents(deviceScope: CoroutineScope) { diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt index 3dece1eb..a4f7601c 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt @@ -1,5 +1,6 @@ package io.rebble.cobble.shared.handlers +import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.packets.AppRunStateMessage import kotlinx.coroutines.CoroutineScope @@ -24,6 +25,7 @@ class AppRunStateHandler( for (message in pebbleDevice.appRunStateService.receivedMessages) { when (message) { is AppRunStateMessage.AppRunStateStart -> { + Logging.v("App started: ${message.uuid.get()}") pebbleDevice.currentActiveApp.value = message.uuid.get() } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/PKJSLifecycleHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/PKJSLifecycleHandler.kt new file mode 100644 index 00000000..1730b70e --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/PKJSLifecycleHandler.kt @@ -0,0 +1,50 @@ +package io.rebble.cobble.shared.handlers + +import io.ktor.client.HttpClient +import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.PlatformContext +import io.rebble.cobble.shared.database.dao.LockerDao +import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.cobble.shared.js.PKJSApp +import io.rebble.cobble.shared.util.File +import io.rebble.libpebblecommon.packets.AppFetchResponseStatus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class PKJSLifecycleHandler( + private val pebbleDevice: PebbleDevice +) : CobbleHandler, KoinComponent { + private val platformContext: PlatformContext by inject() + private val httpClient: HttpClient by inject() + private val lockerDao: LockerDao by inject() + var pkjsApp: PKJSApp? = null + init { + pebbleDevice.negotiationScope.launch { + val deviceScope = pebbleDevice.connectionScope.filterNotNull().first() + listenForPKJSLifecycleChanges(deviceScope) + } + } + + private fun listenForPKJSLifecycleChanges(scope: CoroutineScope) { + pebbleDevice.currentActiveApp.filterNotNull().onEach { + pkjsApp?.stop() + val appFile = getAppPbwFile(platformContext, it.toString()) + if (!appFile.exists()) { + Logging.d("Downloading app $it for js") + downloadPbw(platformContext, httpClient, lockerDao, it.toString()) + } + if (PKJSApp.isJsApp(platformContext, it)) { + Logging.d("Switching to PKJS app $it") + pkjsApp = PKJSApp(it) + pkjsApp?.start(pebbleDevice) + } else { + Logging.v("App $it is not a PKJS app") + } + }.catch { + Logging.e("Error while listening for PKJS lifecycle changes", it) + }.launchIn(scope) + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt index 9b6cad9d..91e55de5 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt @@ -1,9 +1,18 @@ package io.rebble.cobble.shared.js +PKimport io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow -abstract class JsRunner(val appInfo: PbwAppInfo, val jsPath: String) { +abstract class JsRunner(val appInfo: PbwAppInfo, val jsPath: String, val device: PebbleDevice) { abstract suspend fun start() abstract suspend fun stop() abstract fun loadUrl(url: String) + abstract suspend fun signalNewAppMessageData(data: String?): Boolean + abstract suspend fun signalAppMessageAck(data: String?): Boolean + abstract suspend fun signalAppMessageNack(data: String?): Boolean + + protected val _outgoingAppMessages = MutableSharedFlow(extraBufferCapacity = 1) + val outgoingAppMessages = _outgoingAppMessages.asSharedFlow() } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt index 4832ec86..becc409a 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt @@ -1,10 +1,11 @@ package io.rebble.cobble.shared.js +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent expect object JsRunnerFactory: KoinComponent { - fun createJsRunner(scope: CoroutineScope, connectedAddress: String, appInfo: PbwAppInfo, jsPath: String): JsRunner + fun createJsRunner(scope: CoroutineScope, device: PebbleDevice, appInfo: PbwAppInfo, jsPath: String): JsRunner } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt index b7057aa3..36615d5c 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt @@ -1,16 +1,23 @@ package io.rebble.cobble.shared.js import com.benasher44.uuid.Uuid +import com.benasher44.uuid.uuidFrom +import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.handlers.OutgoingMessage import io.rebble.cobble.shared.handlers.getAppPbwFile import io.rebble.cobble.shared.util.getPbwJsFilePath import io.rebble.cobble.shared.util.requirePbwAppInfo import io.rebble.cobble.shared.util.requirePbwJsFilePath -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.withTimeout +import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo +import io.rebble.libpebblecommon.packets.AppMessage +import io.rebble.libpebblecommon.packets.AppMessageTuple +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.* import okio.buffer import okio.use import org.koin.core.component.KoinComponent @@ -22,6 +29,7 @@ class PKJSApp(val uuid: Uuid): KoinComponent { private val appInfo = requirePbwAppInfo(pbw) private val jsPath = requirePbwJsFilePath(context, appInfo, pbw) private var jsRunner: JsRunner? = null + private var runningScope: CoroutineScope? = null companion object { fun isJsApp(context: PlatformContext, uuid: Uuid): Boolean { @@ -31,15 +39,117 @@ class PKJSApp(val uuid: Uuid): KoinComponent { } suspend fun start(device: PebbleDevice) { - withTimeout(1000) { - val connectionScope = device.connectionScope.filterNotNull().first() - jsRunner = JsRunnerFactory.createJsRunner(connectionScope, device.address, appInfo, jsPath) + val connectionScope = withTimeout(1000) { + device.connectionScope.filterNotNull().first() } + val scope = connectionScope + SupervisorJob() + CoroutineName("PKJSApp-$uuid") + runningScope = scope + jsRunner = JsRunnerFactory.createJsRunner(scope, device, appInfo, jsPath) + device.incomingAppMessages.onEach { + if (it is AppMessage.AppMessagePush && it.uuid.get() != uuid) { + Logging.v("Ignoring app message for different app: ${it.uuid.get()} != $uuid") + return@onEach + } + Logging.d("Received app message: $it") + withTimeout(1000) { + device.outgoingAppMessages.emit(OutgoingMessage.Ack(AppMessage.AppMessageACK(it.transactionId.get()))) + } + when (it) { + is AppMessage.AppMessagePush -> jsRunner?.signalNewAppMessageData(it.dictionary.list.toJSDataString(appInfo.appKeys)) + is AppMessage.AppMessageACK -> jsRunner?.signalAppMessageAck(it.toJSDataString()) + is AppMessage.AppMessageNACK -> jsRunner?.signalAppMessageNack(it.toJSDataString()) + } + }.catch { + Logging.e("Error receiving app message", it) + }.launchIn(scope) + jsRunner?.outgoingAppMessages?.onEach { + Logging.d("Sending app message: $it") + val appMessage = AppMessage.fromJSDataString(it, appInfo) + val tID = device.appMessageTransactionSequence.next() + appMessage.transactionId.set(tID) + device.appMessageService.send(appMessage) + }?.catch { + Logging.e("Error sending app message", it) + }?.launchIn(scope) ?: error("JsRunner not initialized") jsRunner?.start() } suspend fun stop() { jsRunner?.stop() + runningScope?.cancel() jsRunner = null } +} + +fun List.toJSDataString(appKeys: Map): String { + val obj = buildJsonObject { + for (tuple in this@toJSDataString) { + val type = tuple.type.get() + val keyId = tuple.key.get() + val key = appKeys.entries.firstOrNull { it.value.toUInt() == keyId }?.key ?: keyId.toString() + + when (type) { + AppMessageTuple.Type.ByteArray.value -> { + val array = buildJsonArray { + for (byte in tuple.dataAsBytes) { + add(byte.toInt()) + } + } + put(key, array) + } + AppMessageTuple.Type.CString.value -> { + put(key, tuple.dataAsString) + } + AppMessageTuple.Type.UInt.value -> { + put(key, tuple.dataAsUnsignedNumber) + } + AppMessageTuple.Type.Int.value -> { + put(key, tuple.dataAsSignedNumber) + } + } + } + } + return Json.encodeToString(obj) +} + +fun AppMessage.AppMessageNACK.toJSDataString(): String { + return buildJsonObject { + put("transactionId", transactionId.get().toInt()) + }.toString() +} + +fun AppMessage.AppMessageACK.toJSDataString(): String { + return buildJsonObject { + put("transactionId", transactionId.get().toInt()) + }.toString() +} + +fun AppMessage.Companion.fromJSDataString(json: String, appInfo: PbwAppInfo): AppMessage { + val jsonElement = Json.parseToJsonElement(json) + val jsonObject = jsonElement.jsonObject + val tuples = jsonObject.mapNotNull { objectEntry -> + val key = objectEntry.key + val keyId = appInfo.appKeys[key] ?: return@mapNotNull null + when (objectEntry.value) { + is JsonArray -> { + AppMessageTuple.createUByteArray(keyId.toUInt(), objectEntry.value.jsonArray.map { it.jsonPrimitive.long.toUByte() }.toUByteArray()) + } + is JsonObject -> error("Invalid JSON value, JsonObject not supported") + else -> { + when { + objectEntry.value.jsonPrimitive.isString -> { + AppMessageTuple.createString(keyId.toUInt(), objectEntry.value.jsonPrimitive.content) + } + objectEntry.value.jsonPrimitive.intOrNull != null -> { + AppMessageTuple.createInt(keyId.toUInt(), objectEntry.value.jsonPrimitive.long.toInt()) + } + objectEntry.value.jsonPrimitive.longOrNull != null -> { + AppMessageTuple.createUInt(keyId.toUInt(), objectEntry.value.jsonPrimitive.long.toUInt()) + } + else -> error("Invalid JSON value, unsupported primitive type") + } + } + } + } + return AppMessage.AppMessagePush(uuid = uuidFrom(appInfo.uuid), tuples = tuples) } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PrivatePKJSInterface.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PrivatePKJSInterface.kt index 06f45474..f57850b1 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PrivatePKJSInterface.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PrivatePKJSInterface.kt @@ -4,5 +4,6 @@ interface PrivatePKJSInterface { fun privateLog(message: String) fun logInterceptedSend() fun logInterceptedRequest() + fun logLocationRequest() fun getVersionCode(): Int } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt index a2ce89c0..308b082b 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt @@ -24,7 +24,7 @@ class LockerViewModel(private val lockerDao: LockerDao): ViewModel() { val entriesState = MutableStateFlow(LockerEntriesState.Loading) init { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO + CoroutineName("LockerViewModelGet")) { lockerDao.getAllEntriesFlow().catch { Logging.e("Error loading locker entries", it) entriesState.value = LockerEntriesState.Error diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/RunningFirmwareVersionParsed.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/RunningFirmwareVersionParsed.kt new file mode 100644 index 00000000..7bf69863 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/RunningFirmwareVersionParsed.kt @@ -0,0 +1,23 @@ +package io.rebble.cobble.shared.util + +import io.rebble.libpebblecommon.packets.WatchFirmwareVersion + +data class RunningFirmwareVersionParsed( + val major: Int, + val minor: Int, + val patch: Int, + val suffix: String +) + +fun WatchFirmwareVersion.parsed(): RunningFirmwareVersionParsed { + val pattern = Regex("[v]?([\\d]+)\\.([\\d]+)\\.?([\\d]*)[\\-]?([\\w\\-\\.]*)") + val match = pattern.matchEntire(this.versionTag.get()) + ?: throw IllegalArgumentException("Invalid version tag: ${this.versionTag.get()}") + val (major, minor, patch, suffix) = match.destructured + return RunningFirmwareVersionParsed( + major = major.toInt(), + minor = minor.toInt(), + patch = if (patch.isEmpty()) 0 else patch.toInt(), + suffix = suffix + ) +} \ No newline at end of file diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.ios.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.ios.kt index c6f72c02..42e64f20 100644 --- a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.ios.kt +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.ios.kt @@ -1,7 +1,9 @@ package io.rebble.cobble.shared.handlers +import io.ktor.client.HttpClient import io.ktor.utils.io.ByteReadChannel import io.rebble.cobble.shared.PlatformContext +import io.rebble.cobble.shared.database.dao.LockerDao import io.rebble.cobble.shared.util.File actual fun getAppPbwFile( @@ -17,4 +19,8 @@ actual suspend fun savePbwFile( byteReadChannel: ByteReadChannel ): String { TODO("Not yet implemented") +} + +actual suspend fun downloadPbw(context: PlatformContext, httpClient: HttpClient, lockerDao: LockerDao, appUuid: String): String? { + TODO("Not yet implemented") } \ No newline at end of file diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt index 303ffa60..16a4db53 100644 --- a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt @@ -1,10 +1,11 @@ package io.rebble.cobble.shared.js +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent actual object JsRunnerFactory: KoinComponent { - actual fun createJsRunner(scope: CoroutineScope, connectedAddress: String, appInfo: PbwAppInfo, jsPath: String): JsRunner = TODO() + actual fun createJsRunner(scope: CoroutineScope, device: PebbleDevice, appInfo: PbwAppInfo, jsPath: String): JsRunner = TODO() } \ No newline at end of file From c2a7b7cbee18bfa93bf6e497cc15c590a58d3a53 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 4 Oct 2024 19:02:34 +0100 Subject: [PATCH 16/20] =?UTF-8?q?typo=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt index 91e55de5..597c8a55 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunner.kt @@ -1,6 +1,6 @@ package io.rebble.cobble.shared.js -PKimport io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow From 67644dfb48c722dae2ef61baf30fbcb48fd50166 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 5 Oct 2024 03:57:17 +0100 Subject: [PATCH 17/20] search, sort alphabetically --- .../cobble/shared/jobs/LockerSyncJob.kt | 2 +- .../rebble/cobble/shared/ui/common/Icons.kt | 2 +- .../shared/ui/view/home/HomeScaffold.kt | 22 +++++++- .../shared/ui/view/home/locker/Locker.kt | 55 +++++++++++++++---- .../ui/view/home/locker/LockerAppList.kt | 17 ++++-- .../view/home/locker/LockerWatchfaceList.kt | 7 ++- .../shared/ui/viewmodel/LockerViewModel.kt | 1 + 7 files changed, 85 insertions(+), 21 deletions(-) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt index 10f1b987..affccd2e 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt @@ -64,7 +64,7 @@ class LockerSyncJob: KoinComponent { } private suspend fun syncToDevice(): Boolean { - val entries = lockerDao.getEntriesForSync() + val entries = lockerDao.getEntriesForSync().sortedBy { it.entry.title } val connectedWatch = ConnectionStateManager.connectionState.value.watchOrNull connectedWatch?.let { val connectedWatchType = WatchHardwarePlatform diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/Icons.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/Icons.kt index e4092a16..695a07e4 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/Icons.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/Icons.kt @@ -57,7 +57,7 @@ object RebbleIcons { @Composable fun plusAdd(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe814), modifier = modifier) @Composable - fun search(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe815), modifier = modifier) + fun search(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe815), modifier = modifier, contentDescription = "Search") @Composable fun dictationMicrophone(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe816), modifier = modifier) @Composable diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt index 4b2e9c8b..6eea9a5c 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt @@ -1,11 +1,14 @@ package io.rebble.cobble.shared.ui.view.home +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.dp import io.rebble.cobble.shared.ui.common.RebbleIcons import io.rebble.cobble.shared.ui.nav.Routes @@ -22,6 +25,7 @@ open class HomePage { fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) { val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() + val searchingState = remember { mutableStateOf(false) } Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) }, /*topBar = { @@ -48,11 +52,27 @@ fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) { ) } }, + floatingActionButton = { + when (page) { + is HomePage.Locker -> { + FloatingActionButton( + modifier = Modifier + .padding(16.dp), + onClick = { + searchingState.value = true + }, + content = { + RebbleIcons.search() + }, + ) + } + } + } ) { innerPadding -> Box(modifier = Modifier.padding(innerPadding)) { when (page) { is HomePage.Locker -> { - Locker(page.tab, onTabChanged = { + Locker(searchingState, page.tab, onTabChanged = { onNavChange(it.navRoute) }) } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt index 4fdbb625..6e09619e 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt @@ -1,20 +1,24 @@ package io.rebble.cobble.shared.ui.view.home.locker +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import io.rebble.cobble.shared.database.dao.LockerDao -import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms import io.rebble.cobble.shared.ui.nav.Routes -import io.rebble.cobble.shared.ui.viewmodel.LockerItemViewModel import io.rebble.cobble.shared.ui.viewmodel.LockerViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO -import kotlinx.coroutines.launch import org.koin.compose.getKoin enum class LockerTabs(val label: String, val navRoute: String) { @@ -23,20 +27,49 @@ enum class LockerTabs(val label: String, val navRoute: String) { } @Composable -fun Locker(page: LockerTabs, lockerDao: LockerDao = getKoin().get(), viewModel: LockerViewModel = viewModel { LockerViewModel(lockerDao) }, onTabChanged: (LockerTabs) -> Unit) { +fun Locker(searchingState: MutableState, page: LockerTabs, lockerDao: LockerDao = getKoin().get(), viewModel: LockerViewModel = viewModel { LockerViewModel(lockerDao) }, onTabChanged: (LockerTabs) -> Unit) { val entriesState: LockerViewModel.LockerEntriesState by viewModel.entriesState.collectAsState() val modalSheetState by viewModel.modalSheetState.collectAsState() val watchIsConnected by viewModel.watchIsConnected.collectAsState() + val searchQuery: String? by viewModel.searchQuery.collectAsState() + val focusRequester = remember { FocusRequester() } + val (searching, setSearching) = searchingState Column { Surface { Row(modifier = Modifier.fillMaxWidth().height(64.dp)) { - LockerTabs.entries.forEachIndexed { index, it -> - NavigationBarItem( - selected = page == it, - onClick = { onTabChanged(it) }, - icon = { Text(it.label) }, + if (searching) { + TextField( + value = searchQuery ?: "", + onValueChange = { viewModel.searchQuery.value = it }, + label = { Text("Search") }, + modifier = Modifier.fillMaxWidth().padding(8.dp) + .focusRequester(focusRequester) + .onGloballyPositioned { + focusRequester.requestFocus() + }, + singleLine = true, + trailingIcon = { + IconButton( + onClick = { + viewModel.searchQuery.value = null + setSearching(false) + }, + modifier = Modifier.align(CenterVertically), + content = { + Icon(Icons.Default.Close, contentDescription = "Clear search") + }, + ) + } ) + } else { + LockerTabs.entries.forEachIndexed { index, it -> + NavigationBarItem( + selected = page == it, + onClick = { onTabChanged(it) }, + icon = { Text(it.label) }, + ) + } } } } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerAppList.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerAppList.kt index 3d2729f4..a36848aa 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerAppList.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerAppList.kt @@ -28,7 +28,10 @@ fun LockerAppList(viewModel: LockerViewModel, onOpenModalSheet: (LockerItemViewM val lazyListState = rememberLazyListState() val koin = getKoin() val entriesState by viewModel.entriesState.collectAsState() - val entries = ((entriesState as? LockerViewModel.LockerEntriesState.Loaded)?.entries ?: emptyList()).filter { it.entry.type == "watchapp" } + val searchQuery: String? by viewModel.searchQuery.collectAsState() + val entries = ((entriesState as? LockerViewModel.LockerEntriesState.Loaded)?.entries ?: emptyList()) + .filter { it.entry.type == "watchapp" } + .filter { searchQuery == null || it.entry.title.contains(searchQuery!!, ignoreCase = true) || it.entry.developerName.contains(searchQuery!!, ignoreCase = true) } val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to -> val entry = entries.first { it.entry.id == from.key } val nwList = entries.toMutableList() @@ -41,11 +44,13 @@ fun LockerAppList(viewModel: LockerViewModel, onOpenModalSheet: (LockerItemViewM items(entries.size, key = { i -> entries[i].entry.id }) { i -> ReorderableItem(state = reorderableLazyListState, key = entries[i].entry.id) { isDragging -> LockerListItem(koin.get(), entries[i], onOpenModalSheet = onOpenModalSheet, dragHandle = { - IconButton( - modifier = Modifier.draggableHandle(), - content = { RebbleIcons.dragHandle() }, - onClick = {} - ) + if (searchQuery == null) { + IconButton( + modifier = Modifier.draggableHandle(), + content = { RebbleIcons.dragHandle() }, + onClick = {} + ) + } }) } } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceList.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceList.kt index eecc2a10..d3368273 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceList.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceList.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,8 +20,12 @@ import io.rebble.cobble.shared.ui.viewmodel.LockerViewModel @Composable fun LockerWatchfaceList(viewModel: LockerViewModel, onOpenModalSheet: (LockerItemViewModel) -> Unit) { + val searchQuery: String? by viewModel.searchQuery.collectAsState() val entriesState: LockerViewModel.LockerEntriesState by viewModel.entriesState.collectAsState() - val entries = ((entriesState as? LockerViewModel.LockerEntriesState.Loaded)?.entries ?: emptyList()).filter { it.entry.type == "watchface" } + val entries = ((entriesState as? LockerViewModel.LockerEntriesState.Loaded)?.entries ?: emptyList()) + .filter { it.entry.type == "watchface" } + .filter { searchQuery == null || it.entry.title.contains(searchQuery!!, ignoreCase = true) || it.entry.developerName.contains(searchQuery!!, ignoreCase = true) } + .sortedBy { it.entry.title } val connectedState: Boolean by viewModel.watchIsConnected.collectAsState() LazyVerticalGrid( diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt index 308b082b..0b4cf4f1 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt @@ -39,6 +39,7 @@ class LockerViewModel(private val lockerDao: LockerDao): ViewModel() { private val _modalSheetState = MutableStateFlow(ModalSheetState.Closed) val modalSheetState: StateFlow = _modalSheetState val watchIsConnected = ConnectionStateManager.isConnected + val searchQuery = MutableStateFlow(null) suspend fun updateOrder(entries: List) { lastJob?.cancel() From f291e76d06b423b84aa7f466586357825e4db62a Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 5 Oct 2024 14:59:44 +0100 Subject: [PATCH 18/20] support installing watchfaces outside of locker limit --- .../cobble/shared/database/dao/LockerDao.kt | 6 ++ .../database/entity/SyncedLockerEntry.kt | 23 +++++++ .../cobble/shared/jobs/LockerSyncJob.kt | 16 ++--- .../ui/viewmodel/LockerItemViewModel.kt | 65 ++++++++++++++++++- 4 files changed, 99 insertions(+), 11 deletions(-) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/LockerDao.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/LockerDao.kt index ca91b316..2f2f606a 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/LockerDao.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/LockerDao.kt @@ -55,4 +55,10 @@ interface LockerDao { @Query("DELETE FROM SyncedLockerEntry") suspend fun clearAll() + + @Query("SELECT COUNT(*) FROM SyncedLockerEntry WHERE nextSyncAction = :action") + suspend fun countEntriesWithNextSyncAction(action: NextSyncAction): Int + + @Query("SELECT * FROM SyncedLockerEntry WHERE nextSyncAction = 'Nothing'") + suspend fun getSyncedEntries(): List } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt index d8853bdc..3ce4e825 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt @@ -2,6 +2,8 @@ package io.rebble.cobble.shared.database.entity import androidx.room.* import io.rebble.cobble.shared.database.NextSyncAction +import io.rebble.cobble.shared.util.AppCompatibility +import io.rebble.libpebblecommon.metadata.WatchType @Entity( indices = [ @@ -41,6 +43,21 @@ data class SyncedLockerEntryWithPlatforms( val platforms: List ) +fun SyncedLockerEntryWithPlatforms.getBestPlatformForDevice(watchType: WatchType): SyncedLockerEntryPlatform? { + val platformName = AppCompatibility.getBestVariant( + watchType, + this.platforms.map { plt -> plt.name } + )?.codename + return this.platforms.firstOrNull { plt -> plt.name == platformName } +} + +fun SyncedLockerEntryWithPlatforms.getVersion(): Pair { + val versionCode = Regex("""\d+\.\d+""").find(entry.version)?.value ?: "0.0" + val appVersionMajor = versionCode.split(".")[0].toUByte() + val appVersionMinor = versionCode.split(".")[1].toUByte() + return Pair(appVersionMajor, appVersionMinor) +} + @Entity( foreignKeys = [ androidx.room.ForeignKey( @@ -66,6 +83,12 @@ data class SyncedLockerEntryPlatform( val images: SyncedLockerEntryPlatformImages, ) +fun SyncedLockerEntryPlatform.getSdkVersion(): Pair { + val sdkVersionMajor = sdkVersion.split(".")[0].toUByte() + val sdkVersionMinor = sdkVersion.split(".")[1].toUByte() + return Pair(sdkVersionMajor, sdkVersionMinor) +} + data class SyncedLockerEntryPlatformImages( val icon: String?, val list: String?, diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt index affccd2e..a9f99c0f 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt @@ -7,6 +7,9 @@ import io.rebble.cobble.shared.api.RWS import io.rebble.cobble.shared.database.NextSyncAction import io.rebble.cobble.shared.database.dao.LockerDao import io.rebble.cobble.shared.database.entity.dataEqualTo +import io.rebble.cobble.shared.database.entity.getBestPlatformForDevice +import io.rebble.cobble.shared.database.entity.getSdkVersion +import io.rebble.cobble.shared.database.entity.getVersion import io.rebble.cobble.shared.domain.api.appstore.toEntity import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager @@ -74,19 +77,12 @@ class LockerSyncJob: KoinComponent { 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 platform = row.getBestPlatformForDevice(connectedWatchType.watchType) 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 (appVersionMajor, appVersionMinor) = row.getVersion() + val (sdkVersionMajor, sdkVersionMinor) = platform.getSdkVersion() val packet = BlobCommand.InsertCommand( Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), BlobCommand.BlobDatabase.App, diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt index 4075eb38..f8c40167 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt @@ -9,19 +9,32 @@ import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.http.HttpStatusCode import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.database.NextSyncAction +import io.rebble.cobble.shared.database.dao.LockerDao import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms +import io.rebble.cobble.shared.database.entity.getBestPlatformForDevice +import io.rebble.cobble.shared.database.entity.getSdkVersion +import io.rebble.cobble.shared.database.entity.getVersion import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform +import io.rebble.libpebblecommon.packets.blobdb.AppMetadata +import io.rebble.libpebblecommon.packets.blobdb.BlobCommand +import io.rebble.libpebblecommon.structmapper.SUUID +import io.rebble.libpebblecommon.structmapper.StructMapper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.decodeToImageBitmap +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import kotlin.random.Random -class LockerItemViewModel(private val httpClient: HttpClient, val entry: SyncedLockerEntryWithPlatforms): ViewModel() { +class LockerItemViewModel(private val httpClient: HttpClient, val entry: SyncedLockerEntryWithPlatforms): ViewModel(), KoinComponent { + private val lockerDao: LockerDao by inject() open class ImageState { object Loading : ImageState() object Error : ImageState() @@ -60,6 +73,56 @@ class LockerItemViewModel(private val httpClient: HttpClient, val entry: SyncedL check(entry.entry.type == "watchface") { "Only watchfaces can be applied" } val watch = ConnectionStateManager.connectionState.value.watchOrNull viewModelScope.launch(Dispatchers.IO) { + if (entry.entry.nextSyncAction == NextSyncAction.Upload && lockerDao.countEntriesWithNextSyncAction(NextSyncAction.Nothing) >= 99) { + Logging.i("Requested watchface isn't in blobdb because there's too many, swapping it out") + val oldest = lockerDao.getSyncedEntries().minByOrNull { it.hearts } ?: run { + Logging.e("No watchfaces to drop") + return@launch + } + + val blobDBService = watch?.blobDBService ?: run { + Logging.e("No connected watch") + return@launch + } + + val watchPlatform = WatchHardwarePlatform + .fromProtocolNumber(watch.metadata.value?.running?.hardwarePlatform?.get() ?: 0u) + val platform = entry.getBestPlatformForDevice(watchPlatform?.watchType ?: return@launch) ?: run { + Logging.e("No platform for watch") + return@launch + } + val (appVersionMajor, appVersionMinor) = entry.getVersion() + val (sdkVersionMajor, sdkVersionMinor) = platform.getSdkVersion() + + blobDBService.send( + BlobCommand.DeleteCommand( + Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), + BlobCommand.BlobDatabase.App, + SUUID(StructMapper(), uuidFrom(oldest.uuid)).toBytes() + ) + ) + + blobDBService.send( + BlobCommand.InsertCommand( + Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), + BlobCommand.BlobDatabase.App, + SUUID(StructMapper(), uuidFrom(entry.entry.uuid)).toBytes(), + AppMetadata().also { meta -> + meta.uuid.set(uuidFrom(entry.entry.uuid)) + meta.flags.set(platform.processInfoFlags.toUInt()) + meta.icon.set(entry.entry.pbwIconResourceId.toUInt()) + meta.appVersionMajor.set(appVersionMajor) + meta.appVersionMinor.set(appVersionMinor) + meta.sdkVersionMajor.set(sdkVersionMajor) + meta.sdkVersionMinor.set(sdkVersionMinor) + meta.appName.set(entry.entry.title) + }.toBytes() + ) + ) + + lockerDao.setNextSyncAction(oldest.id, NextSyncAction.Upload) + lockerDao.setNextSyncAction(entry.entry.id, NextSyncAction.Nothing) + } watch?.appRunStateService?.startApp(uuidFrom(entry.entry.uuid)) } } From 54a51542aefd1e62cf13ac0ca8907bc2f323bbfb Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 7 Oct 2024 16:11:42 +0100 Subject: [PATCH 19/20] optimise imports --- .../rebble/cobble/shared/js/WebViewJsRunnerTest.kt | 2 -- .../kotlin/io/rebble/cobble/FlutterMainActivity.kt | 3 +-- .../main/kotlin/io/rebble/cobble/MainActivity.kt | 3 --- .../io/rebble/cobble/bluetooth/ConnectionLooper.kt | 3 --- .../io/rebble/cobble/bluetooth/DeviceTransport.kt | 4 +--- .../cobble/bridges/common/AppLogFlutterBridge.kt | 3 +-- .../bridges/common/ConnectionFlutterBridge.kt | 8 ++++---- .../io/rebble/cobble/bridges/common/KMPApiBridge.kt | 1 - .../bridges/common/NotificationsFlutterBridge.kt | 2 -- .../bridges/common/ScreenshotsFlutterBridge.kt | 1 - .../bridges/common/TimelineControlFlutterBridge.kt | 1 - .../bridges/ui/CalendarControlFlutterBridge.kt | 2 -- .../rebble/cobble/bridges/ui/DebugFlutterBridge.kt | 3 +-- .../ui/FirmwareUpdateControlFlutterBridge.kt | 4 +--- .../bridges/ui/PermissionControlFlutterBridge.kt | 4 ++-- .../cobble/bridges/ui/WorkaroundsFlutterBridge.kt | 2 +- .../io/rebble/cobble/data/MetadataConversion.kt | 2 +- .../io/rebble/cobble/di/ActivitySubcomponent.kt | 6 ------ .../main/kotlin/io/rebble/cobble/di/AppComponent.kt | 1 - .../main/kotlin/io/rebble/cobble/di/AppModule.kt | 7 +------ .../io/rebble/cobble/di/FlutterActivityModule.kt | 1 - .../rebble/cobble/di/FlutterActivitySubcomponent.kt | 2 -- .../kotlin/io/rebble/cobble/log/LogSendingTask.kt | 5 ++++- .../kotlin/io/rebble/cobble/pigeons/Pigeons.java | 13 +++++++------ .../io/rebble/cobble/service/InCallService.kt | 1 - .../kotlin/io/rebble/cobble/service/WatchService.kt | 13 +++++++------ android/build.gradle.kts | 1 - .../main/java/io/rebble/cobble/bluetooth/BlueIO.kt | 2 -- .../rebble/cobble/bluetooth/EmulatedPebbleDevice.kt | 3 --- .../io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt | 3 +-- .../cobble/bluetooth/classic/BlueSerialDriver.kt | 7 ++++--- .../bluetooth/classic/ReconnectionSocketServer.kt | 2 -- .../cobble/bluetooth/classic/SocketSerialDriver.kt | 6 ++++-- .../android/kit/ExampleInstrumentedTest.java | 4 ++-- .../pebblekit_android/src/main/AndroidManifest.xml | 2 +- .../com/getpebble/android/kit/ExampleUnitTest.java | 2 +- .../cobble/shared/database/AppDatabase.android.kt | 1 - .../io/rebble/cobble/shared/di/AndroidModule.kt | 8 ++++---- .../cobble/shared/domain/PermissionChangeBus.kt | 1 - .../domain/calendar/PhoneCalendarUtils.android.kt | 5 ++++- .../notifications/CallNotificationProcessor.kt | 8 ++++---- .../cobble/shared/handlers/SystemHandler.android.kt | 1 - .../cobble/shared/handlers/music/MusicHandler.kt | 7 ++++--- .../cobble/shared/jobs/AndroidLockerSyncJob.kt | 5 ++++- .../cobble/shared/js/JsRunnerFactory.android.kt | 1 - .../io/rebble/cobble/shared/js/WebViewJsRunner.kt | 2 -- .../cobble/shared/util/AppInstallUtils.android.kt | 2 -- .../io/rebble/cobble/shared/api/AppstoreClient.kt | 8 ++++---- .../io/rebble/cobble/shared/api/AuthClient.kt | 1 - .../kotlin/io/rebble/cobble/shared/api/RWS.kt | 5 ++++- .../io/rebble/cobble/shared/data/CalendarEvent.kt | 3 --- .../io/rebble/cobble/shared/data/TimelineAction.kt | 3 --- .../io/rebble/cobble/shared/database/AppDatabase.kt | 3 --- .../cobble/shared/database/entity/Calendar.kt | 1 - .../shared/database/entity/NotificationChannel.kt | 1 - .../shared/database/entity/SyncedLockerEntry.kt | 4 ++-- .../cobble/shared/database/entity/TimelinePin.kt | 2 -- .../io/rebble/cobble/shared/di/CalendarModule.kt | 2 +- .../io/rebble/cobble/shared/di/DataStoreModule.kt | 3 +-- .../io/rebble/cobble/shared/di/DatabaseModule.kt | 1 - .../io/rebble/cobble/shared/di/LibPebbleModule.kt | 6 ++---- .../io/rebble/cobble/shared/di/StateModule.kt | 2 -- .../shared/domain/api/appstore/LockerEntry.kt | 3 +-- .../cobble/shared/domain/calendar/CalendarSync.kt | 8 +++----- .../shared/domain/calendar/PhoneCalendarSyncer.kt | 4 ---- .../cobble/shared/domain/common/PebbleDevice.kt | 2 -- .../notifications/NotificationActionHandler.kt | 1 - .../cobble/shared/domain/state/ConnectionState.kt | 5 ++--- .../shared/domain/timeline/TimelineActionManager.kt | 4 ++-- .../shared/domain/timeline/WatchTimelineSyncer.kt | 3 --- .../cobble/shared/handlers/AppInstallHandler.kt | 9 --------- .../cobble/shared/handlers/AppMessageHandler.kt | 6 ------ .../cobble/shared/handlers/AppRunStateHandler.kt | 2 -- .../cobble/shared/handlers/CalendarActionHandler.kt | 3 --- .../cobble/shared/handlers/PKJSLifecycleHandler.kt | 2 -- .../rebble/cobble/shared/handlers/SystemHandler.kt | 6 ++++-- .../io/rebble/cobble/shared/jobs/LockerSyncJob.kt | 4 ---- .../io/rebble/cobble/shared/js/JsRunnerFactory.kt | 1 - .../kotlin/io/rebble/cobble/shared/js/PKJSApp.kt | 3 --- .../cobble/shared/middleware/DeviceLogController.kt | 4 +++- .../cobble/shared/middleware/PutBytesController.kt | 2 -- .../kotlin/io/rebble/cobble/shared/ui/Theme.kt | 1 - .../cobble/shared/ui/common/AppIconContainer.kt | 1 - .../io/rebble/cobble/shared/ui/common/TextIcon.kt | 3 --- .../cobble/shared/ui/view/home/HomeScaffold.kt | 7 ++++--- .../rebble/cobble/shared/ui/view/home/TestPage.kt | 2 -- .../cobble/shared/ui/view/home/locker/Locker.kt | 3 --- .../shared/ui/view/home/locker/LockerAppList.kt | 13 ++++--------- .../shared/ui/view/home/locker/LockerItemSheet.kt | 6 +++--- .../shared/ui/view/home/locker/LockerListItem.kt | 1 - .../ui/view/home/locker/LockerWatchfaceItem.kt | 3 --- .../ui/view/home/locker/LockerWatchfaceList.kt | 6 ------ .../cobble/shared/ui/viewmodel/LockerViewModel.kt | 4 +++- .../io/rebble/cobble/shared/util/AppInstallUtils.kt | 2 -- .../kotlin/io/rebble/cobble/shared/di/IosModule.kt | 1 - .../rebble/cobble/shared/js/JsRunnerFactory.ios.kt | 1 - .../cobble/shared/util/AppInstallUtils.ios.kt | 1 - 97 files changed, 108 insertions(+), 234 deletions(-) diff --git a/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt b/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt index 0d5774ba..adc68494 100644 --- a/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt +++ b/android/app/src/androidTest/kotlin/io/rebble/cobble/shared/js/WebViewJsRunnerTest.kt @@ -8,8 +8,6 @@ import io.rebble.libpebblecommon.util.runBlocking import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.json.Json import org.junit.Before import org.junit.Test diff --git a/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt b/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt index 72b6506e..ae25ffb6 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/FlutterMainActivity.kt @@ -13,10 +13,9 @@ import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.REQUEST_CODE_NOTIFICATIONS_POST -import io.rebble.cobble.shared.domain.PermissionChangeBus import io.rebble.cobble.service.CompanionDeviceService import io.rebble.cobble.service.InCallService -import io.rebble.cobble.shared.database.closeDatabase +import io.rebble.cobble.shared.domain.PermissionChangeBus import io.rebble.cobble.shared.util.hasNotificationPostingPermission import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.plus diff --git a/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt b/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt index b36ad7a2..7d8dfdb0 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt @@ -6,14 +6,11 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat -import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import io.rebble.cobble.shared.ui.view.MainView import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import kotlinx.coroutines.plus class MainActivity : AppCompatActivity() { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index 5de8355e..9c48628f 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -6,7 +6,6 @@ import android.content.Context import android.os.Build import androidx.annotation.RequiresPermission import io.rebble.cobble.bluetooth.classic.ReconnectionSocketServer -import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull @@ -15,8 +14,6 @@ import kotlinx.coroutines.flow.* import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.min @OptIn(ExperimentalCoroutinesApi::class) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index 11d68d1b..80289472 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -13,14 +13,12 @@ import io.rebble.cobble.bluetooth.classic.BlueSerialDriver import io.rebble.cobble.bluetooth.classic.SocketSerialDriver import io.rebble.cobble.bluetooth.scan.BleScanner import io.rebble.cobble.bluetooth.scan.ClassicScanner -import io.rebble.cobble.shared.datastore.FlutterPreferences import io.rebble.cobble.datasources.IncomingPacketsListener +import io.rebble.cobble.shared.datastore.FlutterPreferences import io.rebble.cobble.shared.domain.common.PebbleDevice -import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.onCompletion -import org.koin.mp.KoinPlatformTools import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppLogFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppLogFlutterBridge.kt index da40f647..bda2ee1b 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppLogFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppLogFlutterBridge.kt @@ -2,11 +2,10 @@ package io.rebble.cobble.bridges.common import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController -import io.rebble.cobble.shared.middleware.AppLogController import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull -import kotlinx.coroutines.CoroutineScope +import io.rebble.cobble.shared.middleware.AppLogController import kotlinx.coroutines.Job import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt index 1f3744a4..04710f03 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt @@ -8,11 +8,11 @@ import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull -import io.rebble.libpebblecommon.ProtocolHandler -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import javax.inject.Inject class ConnectionFlutterBridge @Inject constructor( diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt index 657a29c7..f3de4527 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt @@ -1,6 +1,5 @@ package io.rebble.cobble.bridges.common -import android.content.Context import android.content.Intent import io.rebble.cobble.FlutterMainActivity import io.rebble.cobble.MainActivity diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/NotificationsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/NotificationsFlutterBridge.kt index 9795d6e8..404d30e4 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/NotificationsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/NotificationsFlutterBridge.kt @@ -4,8 +4,6 @@ import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.shared.database.dao.CachedPackageInfoDao -import io.rebble.libpebblecommon.packets.blobdb.PushNotification -import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScreenshotsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScreenshotsFlutterBridge.kt index 64f70b77..c2bd4ac0 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScreenshotsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScreenshotsFlutterBridge.kt @@ -10,7 +10,6 @@ import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.cobble.util.launchPigeonResult import io.rebble.libpebblecommon.packets.* -import io.rebble.libpebblecommon.services.ScreenshotService import io.rebble.libpebblecommon.util.DataBuffer import io.rebble.libpebblecommon.util.ushr import kotlinx.coroutines.CoroutineScope diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/TimelineControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/TimelineControlFlutterBridge.kt index 218748a9..89f1dd27 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/TimelineControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/TimelineControlFlutterBridge.kt @@ -18,7 +18,6 @@ import io.rebble.libpebblecommon.structmapper.SUUID import io.rebble.libpebblecommon.structmapper.StructMapper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import org.koin.core.qualifier.named import org.koin.mp.KoinPlatformTools diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/CalendarControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/CalendarControlFlutterBridge.kt index 006b155d..b20bbaa6 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/CalendarControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/CalendarControlFlutterBridge.kt @@ -3,8 +3,6 @@ package io.rebble.cobble.bridges.ui import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.pigeons.Pigeons.CalendarCallbacks -import io.rebble.cobble.shared.database.dao.CalendarDao import io.rebble.cobble.shared.datastore.KMPPrefs import io.rebble.cobble.shared.domain.calendar.CalendarSync import io.rebble.cobble.shared.domain.state.ConnectionState diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt index d99bf019..ab35d220 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt @@ -2,13 +2,12 @@ package io.rebble.cobble.bridges.ui import android.content.Context import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.shared.errors.GlobalExceptionHandler import io.rebble.cobble.log.collectAndShareLogs import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.shared.datastore.KMPPrefs +import io.rebble.cobble.shared.errors.GlobalExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt index c3a061ec..ea9726cd 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt @@ -4,18 +4,16 @@ import android.content.Context import android.net.Uri import androidx.core.net.toFile import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.shared.middleware.PutBytesController import io.rebble.cobble.pigeons.BooleanWrapper import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull -import io.rebble.cobble.util.launchPigeonResult import io.rebble.cobble.shared.util.zippedSource +import io.rebble.cobble.util.launchPigeonResult import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import io.rebble.libpebblecommon.metadata.pbz.manifest.PbzManifest import io.rebble.libpebblecommon.packets.SystemMessage import io.rebble.libpebblecommon.packets.TimeMessage -import io.rebble.libpebblecommon.services.SystemService import io.rebble.libpebblecommon.util.Crc32Calculator import kotlinx.coroutines.* import kotlinx.coroutines.flow.first diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt index 8114a8a6..5950bd66 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt @@ -13,10 +13,10 @@ import androidx.core.content.getSystemService import androidx.lifecycle.Lifecycle import io.rebble.cobble.FlutterMainActivity import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.shared.domain.PermissionChangeBus -import io.rebble.cobble.shared.domain.notifications.NotificationListener import io.rebble.cobble.pigeons.NumberWrapper import io.rebble.cobble.pigeons.Pigeons +import io.rebble.cobble.shared.domain.PermissionChangeBus +import io.rebble.cobble.shared.domain.notifications.NotificationListener import io.rebble.cobble.util.asFlow import io.rebble.cobble.util.launchPigeonResult import kotlinx.coroutines.CompletableDeferred diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/WorkaroundsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/WorkaroundsFlutterBridge.kt index 48fc6c76..592a160f 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/WorkaroundsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/WorkaroundsFlutterBridge.kt @@ -1,10 +1,10 @@ package io.rebble.cobble.bridges.ui import android.content.Context -import io.rebble.cobble.shared.workarounds.WorkaroundDescriptor import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.pigeons.ListWrapper import io.rebble.cobble.pigeons.Pigeons +import io.rebble.cobble.shared.workarounds.WorkaroundDescriptor import javax.inject.Inject class WorkaroundsFlutterBridge @Inject constructor( diff --git a/android/app/src/main/kotlin/io/rebble/cobble/data/MetadataConversion.kt b/android/app/src/main/kotlin/io/rebble/cobble/data/MetadataConversion.kt index d4f59c3b..864328a4 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/data/MetadataConversion.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/data/MetadataConversion.kt @@ -2,8 +2,8 @@ package io.rebble.cobble.data import io.rebble.cobble.bluetooth.BluetoothPebbleDevice import io.rebble.cobble.bluetooth.EmulatedPebbleDevice -import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.pigeons.Pigeons +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.packets.WatchFirmwareVersion import io.rebble.libpebblecommon.packets.WatchVersion diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/ActivitySubcomponent.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/ActivitySubcomponent.kt index 4a69b8e8..865335f1 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/ActivitySubcomponent.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/ActivitySubcomponent.kt @@ -2,13 +2,7 @@ package io.rebble.cobble.di import dagger.BindsInstance import dagger.Subcomponent -import io.rebble.cobble.FlutterMainActivity import io.rebble.cobble.MainActivity -import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.di.bridges.CommonBridge -import io.rebble.cobble.di.bridges.CommonBridgesModule -import io.rebble.cobble.di.bridges.UiBridge -import io.rebble.cobble.di.bridges.UiBridgesModule import javax.inject.Scope @PerActivity diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt index e9ae14a6..276e0976 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt @@ -9,7 +9,6 @@ import io.rebble.cobble.NotificationChannelManager import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bluetooth.DeviceTransport import io.rebble.cobble.datasources.PairedStorage -import io.rebble.cobble.shared.middleware.DeviceLogController import io.rebble.cobble.service.ServiceLifecycleControl import io.rebble.cobble.shared.database.dao.NotificationChannelDao import io.rebble.cobble.shared.datastore.KMPPrefs diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt index 91b6b22c..9d624827 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt @@ -7,8 +7,6 @@ import android.service.notification.StatusBarNotification import dagger.Binds import dagger.Module import dagger.Provides -import io.rebble.cobble.shared.errors.GlobalExceptionHandler -import io.rebble.cobble.shared.middleware.DeviceLogController import io.rebble.cobble.shared.database.AppDatabase import io.rebble.cobble.shared.database.dao.CachedPackageInfoDao import io.rebble.cobble.shared.database.dao.NotificationChannelDao @@ -17,16 +15,13 @@ import io.rebble.cobble.shared.datastore.FlutterPreferences 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.errors.GlobalExceptionHandler import io.rebble.cobble.shared.jobs.AndroidJobScheduler -import io.rebble.cobble.shared.middleware.PutBytesController -import io.rebble.libpebblecommon.services.LogDumpService import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.plus -import org.koin.core.parameter.parametersOf import org.koin.core.qualifier.named import org.koin.mp.KoinPlatformTools import java.util.UUID diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/FlutterActivityModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/FlutterActivityModule.kt index 0cfdb3e8..a0ff7e78 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/FlutterActivityModule.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/FlutterActivityModule.kt @@ -6,7 +6,6 @@ import dagger.Provides import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.BinaryMessenger import io.rebble.cobble.FlutterMainActivity -import io.rebble.cobble.MainActivity import io.rebble.cobble.bridges.ui.BridgeLifecycleController import kotlinx.coroutines.CoroutineScope diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/FlutterActivitySubcomponent.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/FlutterActivitySubcomponent.kt index 2b235ece..73739f31 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/FlutterActivitySubcomponent.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/FlutterActivitySubcomponent.kt @@ -3,13 +3,11 @@ package io.rebble.cobble.di import dagger.BindsInstance import dagger.Subcomponent import io.rebble.cobble.FlutterMainActivity -import io.rebble.cobble.MainActivity import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.di.bridges.CommonBridge import io.rebble.cobble.di.bridges.CommonBridgesModule import io.rebble.cobble.di.bridges.UiBridge import io.rebble.cobble.di.bridges.UiBridgesModule -import javax.inject.Scope @PerActivity @Subcomponent( diff --git a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt index a57e9087..b72d51d5 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt @@ -17,7 +17,10 @@ import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import io.rebble.libpebblecommon.packets.LogDump import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber diff --git a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java index d4607998..5f43eab1 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java +++ b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java @@ -4,21 +4,22 @@ package io.rebble.cobble.pigeons; import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import io.flutter.plugin.common.BasicMessageChannel; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MessageCodec; -import io.flutter.plugin.common.StandardMessageCodec; + import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; + /** Generated class from Pigeon. */ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) public class Pigeons { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt index 95cd6bc8..fbef5a4f 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt @@ -15,7 +15,6 @@ import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.libpebblecommon.packets.PhoneControl -import io.rebble.libpebblecommon.services.PhoneControlService import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt index 9801b1a3..273d8987 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt @@ -8,20 +8,21 @@ import androidx.annotation.DrawableRes import androidx.core.app.NotificationCompat import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope -import io.rebble.cobble.* +import io.rebble.cobble.CobbleApplication +import io.rebble.cobble.MainActivity +import io.rebble.cobble.R import io.rebble.cobble.bluetooth.BluetoothPebbleDevice import io.rebble.cobble.bluetooth.ConnectionLooper 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.* -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import timber.log.Timber -import javax.inject.Provider @OptIn(ExperimentalCoroutinesApi::class) class WatchService : LifecycleService() { diff --git a/android/build.gradle.kts b/android/build.gradle.kts index c4f3ec3d..df076549 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,4 +1,3 @@ -import org.gradle.api.tasks.Delete import java.util.Properties buildscript { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt index 25fe2b9b..b550a465 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt @@ -4,8 +4,6 @@ import android.Manifest import android.bluetooth.BluetoothDevice import androidx.annotation.RequiresPermission import io.rebble.cobble.shared.domain.common.PebbleDevice -import io.rebble.libpebblecommon.ProtocolHandler -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.isActive diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt index d82bddcc..1b57666b 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/EmulatedPebbleDevice.kt @@ -1,9 +1,6 @@ package io.rebble.cobble.bluetooth -import android.bluetooth.BluetoothDevice import io.rebble.cobble.shared.domain.common.PebbleDevice -import io.rebble.libpebblecommon.ProtocolHandler -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.isActive class EmulatedPebbleDevice( diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index b2e45b81..449392ad 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -2,10 +2,9 @@ package io.rebble.cobble.bluetooth.ble import android.content.Context import io.rebble.cobble.bluetooth.* +import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.workarounds.UnboundWatchBeforeConnecting import io.rebble.cobble.shared.workarounds.WorkaroundDescriptor -import io.rebble.cobble.shared.domain.common.PebbleDevice -import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt index 7a78fb43..3ae7ef9a 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt @@ -2,15 +2,16 @@ package io.rebble.cobble.bluetooth.classic import android.Manifest import androidx.annotation.RequiresPermission -import io.rebble.cobble.bluetooth.* +import io.rebble.cobble.bluetooth.BlueIO +import io.rebble.cobble.bluetooth.BluetoothPebbleDevice +import io.rebble.cobble.bluetooth.ProtocolIO +import io.rebble.cobble.bluetooth.SingleConnectionStatus import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.domain.common.PebbleDevice -import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn import java.io.IOException import java.util.UUID diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt index 163830dc..f45eb3a0 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt @@ -6,9 +6,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.isActive import kotlinx.coroutines.isActive -import kotlinx.coroutines.runInterruptible import timber.log.Timber import java.io.IOException import java.util.UUID diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt index 4fc1771d..bf29849c 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt @@ -1,8 +1,10 @@ package io.rebble.cobble.bluetooth.classic -import io.rebble.cobble.bluetooth.* +import io.rebble.cobble.bluetooth.BlueIO +import io.rebble.cobble.bluetooth.EmulatedPebbleDevice +import io.rebble.cobble.bluetooth.SingleConnectionStatus +import io.rebble.cobble.bluetooth.readFully import io.rebble.cobble.shared.domain.common.PebbleDevice -import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.packets.QemuPacket import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint import kotlinx.coroutines.* diff --git a/android/pebblekit_android/src/androidTest/java/com/getpebble/android/kit/ExampleInstrumentedTest.java b/android/pebblekit_android/src/androidTest/java/com/getpebble/android/kit/ExampleInstrumentedTest.java index 4d5e4c81..8f1638e9 100644 --- a/android/pebblekit_android/src/androidTest/java/com/getpebble/android/kit/ExampleInstrumentedTest.java +++ b/android/pebblekit_android/src/androidTest/java/com/getpebble/android/kit/ExampleInstrumentedTest.java @@ -2,13 +2,13 @@ import android.content.Context; -import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** * Instrumented test, which will execute on an Android device. diff --git a/android/pebblekit_android/src/main/AndroidManifest.xml b/android/pebblekit_android/src/main/AndroidManifest.xml index a5918e68..44008a43 100644 --- a/android/pebblekit_android/src/main/AndroidManifest.xml +++ b/android/pebblekit_android/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/android/pebblekit_android/src/test/java/com/getpebble/android/kit/ExampleUnitTest.java b/android/pebblekit_android/src/test/java/com/getpebble/android/kit/ExampleUnitTest.java index 98ffd6ba..1299938e 100644 --- a/android/pebblekit_android/src/test/java/com/getpebble/android/kit/ExampleUnitTest.java +++ b/android/pebblekit_android/src/test/java/com/getpebble/android/kit/ExampleUnitTest.java @@ -2,7 +2,7 @@ import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** * Example local unit test, which will execute on the development machine (host). diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.android.kt index d4185a17..d6b29fcb 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.android.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.android.kt @@ -2,7 +2,6 @@ package io.rebble.cobble.shared.database import androidx.room.Room import androidx.room.RoomDatabase -import androidx.room.RoomDatabaseConstructor import org.koin.core.context.GlobalContext actual fun getDatabaseBuilder(): RoomDatabase.Builder { diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt index f7d030d3..35dd1d88 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/di/AndroidModule.kt @@ -6,23 +6,23 @@ import io.rebble.cobble.shared.AndroidPlatformContext import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.datastore.FlutterPreferences import io.rebble.cobble.shared.datastore.createDataStore -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.domain.calendar.PlatformCalendarActionExecutor import io.rebble.cobble.shared.domain.common.PebbleDevice +import io.rebble.cobble.shared.domain.notifications.AndroidNotificationActionExecutor import io.rebble.cobble.shared.domain.notifications.CallNotificationProcessor import io.rebble.cobble.shared.domain.notifications.NotificationProcessor +import io.rebble.cobble.shared.domain.notifications.PlatformNotificationActionExecutor import io.rebble.cobble.shared.handlers.* import io.rebble.cobble.shared.handlers.music.MusicHandler import io.rebble.cobble.shared.jobs.AndroidJobScheduler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.koin.dsl.module import org.koin.android.ext.koin.androidContext import org.koin.core.module.dsl.singleOf import org.koin.core.qualifier.named import org.koin.dsl.bind +import org.koin.dsl.module val androidModule = module { factory { diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt index 5a9c3f7a..f826fc60 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/PermissionChangeBus.kt @@ -2,7 +2,6 @@ package io.rebble.cobble.shared.domain import android.content.Context import io.rebble.cobble.shared.util.hasNotificationAccessPermission -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* /** diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/calendar/PhoneCalendarUtils.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/calendar/PhoneCalendarUtils.android.kt index df0991f4..41fde7ca 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/calendar/PhoneCalendarUtils.android.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/calendar/PhoneCalendarUtils.android.kt @@ -10,7 +10,10 @@ import com.philjay.RRule import io.rebble.cobble.shared.AndroidPlatformContext import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.PlatformContext -import io.rebble.cobble.shared.data.* +import io.rebble.cobble.shared.data.CalendarEvent +import io.rebble.cobble.shared.data.EventAttendee +import io.rebble.cobble.shared.data.EventRecurrenceRule +import io.rebble.cobble.shared.data.EventReminder import io.rebble.cobble.shared.database.entity.Calendar import kotlinx.datetime.* diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/CallNotificationProcessor.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/CallNotificationProcessor.kt index 6372c0c7..bfbf6c02 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/CallNotificationProcessor.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/domain/notifications/CallNotificationProcessor.kt @@ -8,17 +8,17 @@ import io.rebble.cobble.shared.domain.notifications.calls.CallNotificationType import io.rebble.cobble.shared.domain.notifications.calls.DiscordCallNotificationInterpreter import io.rebble.cobble.shared.domain.notifications.calls.WhatsAppCallNotificationInterpreter import io.rebble.cobble.shared.domain.state.ConnectionState +import io.rebble.cobble.shared.domain.state.ConnectionStateManager +import io.rebble.cobble.shared.domain.state.watchOrNull +import io.rebble.cobble.shared.errors.GlobalExceptionHandler import io.rebble.libpebblecommon.packets.PhoneControl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlin.random.Random -import io.rebble.cobble.shared.domain.state.ConnectionStateManager -import io.rebble.cobble.shared.domain.state.watchOrNull -import io.rebble.cobble.shared.errors.GlobalExceptionHandler import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import kotlin.random.Random class CallNotificationProcessor: KoinComponent { private val exceptionHandler: GlobalExceptionHandler by inject() diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.android.kt index 479cc98c..5a7d486b 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.android.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.android.kt @@ -10,7 +10,6 @@ import io.rebble.cobble.shared.AndroidPlatformContext import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.util.coroutines.asFlow import io.rebble.libpebblecommon.packets.PhoneAppVersion -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/MusicHandler.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/MusicHandler.kt index 62b0e19d..46a04b44 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/MusicHandler.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/handlers/music/MusicHandler.kt @@ -14,10 +14,11 @@ import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.domain.notificationPermissionFlow import io.rebble.cobble.shared.handlers.CobbleHandler import io.rebble.libpebblecommon.packets.MusicControl -import io.rebble.libpebblecommon.services.MusicService -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.actor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* +import kotlinx.coroutines.job +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject import timber.log.Timber diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/jobs/AndroidLockerSyncJob.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/jobs/AndroidLockerSyncJob.kt index 74059191..0e6c9ad3 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/jobs/AndroidLockerSyncJob.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/jobs/AndroidLockerSyncJob.kt @@ -6,7 +6,10 @@ 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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import kotlin.coroutines.CoroutineContext diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt index df2cc55a..036e811f 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.android.kt @@ -4,7 +4,6 @@ import android.content.Context import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt index 1f5f2c1f..b1964e53 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/js/WebViewJsRunner.kt @@ -13,8 +13,6 @@ import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json diff --git a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.android.kt b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.android.kt index 73af612a..56ea2d83 100644 --- a/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.android.kt +++ b/android/shared/src/androidMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.android.kt @@ -1,6 +1,5 @@ package io.rebble.cobble.shared.util -import com.benasher44.uuid.Uuid import io.rebble.cobble.shared.AndroidPlatformContext import io.rebble.cobble.shared.PlatformContext import io.rebble.libpebblecommon.metadata.WatchType @@ -9,7 +8,6 @@ import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwManifest import kotlinx.serialization.json.decodeFromStream import okio.Source import okio.buffer -import okio.source actual fun getPbwManifest( pbwFile: File, diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AppstoreClient.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AppstoreClient.kt index 26e39563..df0cb6b6 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AppstoreClient.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AppstoreClient.kt @@ -2,11 +2,11 @@ package io.rebble.cobble.shared.api import io.ktor.client.HttpClient import io.ktor.client.call.body -import io.ktor.client.engine.HttpClientEngine -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.request.* +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.headers +import io.ktor.client.request.put import io.ktor.http.HttpHeaders -import io.ktor.serialization.kotlinx.json.json import io.rebble.cobble.shared.domain.api.appstore.LockerEntry import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AuthClient.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AuthClient.kt index e08ae7cc..20d7bbf0 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AuthClient.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/AuthClient.kt @@ -2,7 +2,6 @@ package io.rebble.cobble.shared.api import io.ktor.client.HttpClient import io.ktor.client.call.body -import io.ktor.client.engine.HttpClientEngine import io.ktor.client.request.get import io.ktor.client.request.headers import io.ktor.http.HttpHeaders diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/RWS.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/RWS.kt index b90b0004..9cdb2b37 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/RWS.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/api/RWS.kt @@ -4,7 +4,10 @@ import io.rebble.cobble.shared.domain.state.CurrentToken import io.rebble.cobble.shared.domain.state.CurrentToken.LoggedOut.tokenOrNull import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koin.core.qualifier.named diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/CalendarEvent.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/CalendarEvent.kt index 5572f341..ed452717 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/CalendarEvent.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/CalendarEvent.kt @@ -1,7 +1,6 @@ package io.rebble.cobble.shared.data import com.benasher44.uuid.uuid4 -import com.benasher44.uuid.uuidFrom import io.rebble.cobble.shared.database.NextSyncAction import io.rebble.cobble.shared.database.entity.Calendar import io.rebble.cobble.shared.database.entity.TimelinePin @@ -11,8 +10,6 @@ import io.rebble.cobble.shared.domain.timeline.TimelineIcon import io.rebble.libpebblecommon.packets.blobdb.TimelineItem import io.rebble.libpebblecommon.util.trimWithEllipsis import kotlinx.datetime.Instant -import kotlinx.datetime.LocalDate -import kotlinx.datetime.format.* import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.time.DurationUnit diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/TimelineAction.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/TimelineAction.kt index 105b3441..55b10b5e 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/TimelineAction.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/TimelineAction.kt @@ -1,11 +1,8 @@ package io.rebble.cobble.shared.data -import io.rebble.cobble.shared.domain.calendar.CalendarTimelineActionId -import io.rebble.libpebblecommon.packets.blobdb.TimelineAction import io.rebble.libpebblecommon.packets.blobdb.TimelineItem import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable -import kotlinx.serialization.Serializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt index 02f3a5b9..3951237b 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt @@ -1,15 +1,12 @@ package io.rebble.cobble.shared.database import androidx.room.* -import androidx.room.migration.Migration import androidx.sqlite.driver.bundled.BundledSQLiteDriver import io.rebble.cobble.shared.database.dao.* import io.rebble.cobble.shared.database.entity.* import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.koin.mp.KoinPlatformTools @Database( diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/Calendar.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/Calendar.kt index 30724180..e82dd324 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/Calendar.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/Calendar.kt @@ -1,6 +1,5 @@ package io.rebble.cobble.shared.database.entity -import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/NotificationChannel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/NotificationChannel.kt index ac69b4c0..305d153b 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/NotificationChannel.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/NotificationChannel.kt @@ -1,7 +1,6 @@ package io.rebble.cobble.shared.database.entity import androidx.room.Entity -import androidx.room.PrimaryKey @Entity(primaryKeys = ["packageId", "channelId"]) data class NotificationChannel( diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt index 3ce4e825..19310e0b 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt @@ -60,11 +60,11 @@ fun SyncedLockerEntryWithPlatforms.getVersion(): Pair { @Entity( foreignKeys = [ - androidx.room.ForeignKey( + ForeignKey( entity = SyncedLockerEntry::class, parentColumns = ["id"], childColumns = ["lockerEntryId"], - onDelete = androidx.room.ForeignKey.CASCADE + onDelete = ForeignKey.CASCADE ) ], indices = [ diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/TimelinePin.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/TimelinePin.kt index 412ac877..baecf976 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/TimelinePin.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/TimelinePin.kt @@ -7,8 +7,6 @@ import io.rebble.cobble.shared.database.NextSyncAction import io.rebble.libpebblecommon.packets.blobdb.TimelineItem import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.minutes diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/CalendarModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/CalendarModule.kt index 23d08162..f6726076 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/CalendarModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/CalendarModule.kt @@ -2,8 +2,8 @@ package io.rebble.cobble.shared.di import io.rebble.cobble.shared.domain.calendar.CalendarSync import io.rebble.cobble.shared.domain.calendar.PhoneCalendarSyncer -import io.rebble.cobble.shared.domain.timeline.WatchTimelineSyncer import io.rebble.cobble.shared.domain.timeline.TimelineActionManager +import io.rebble.cobble.shared.domain.timeline.WatchTimelineSyncer import io.rebble.cobble.shared.errors.GlobalExceptionHandler import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DataStoreModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DataStoreModule.kt index ae6caade..c481de4e 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DataStoreModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DataStoreModule.kt @@ -1,9 +1,8 @@ package io.rebble.cobble.shared.di +import io.rebble.cobble.shared.datastore.KMPPrefs import org.koin.core.module.dsl.singleOf import org.koin.dsl.module -import io.rebble.cobble.shared.datastore.KMPPrefs -import io.rebble.cobble.shared.datastore.createDataStore val dataStoreModule = module { singleOf(::KMPPrefs) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DatabaseModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DatabaseModule.kt index 2d3733d4..17d67917 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DatabaseModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/DatabaseModule.kt @@ -2,7 +2,6 @@ package io.rebble.cobble.shared.di import io.rebble.cobble.shared.database.AppDatabase import io.rebble.cobble.shared.database.getDatabase -import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val databaseModule = module { diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt index 2d18cbc4..e23a401c 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/LibPebbleModule.kt @@ -1,17 +1,15 @@ package io.rebble.cobble.shared.di -import io.rebble.cobble.shared.Logging -import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.middleware.PutBytesController -import io.rebble.libpebblecommon.ProtocolHandlerImpl import io.rebble.libpebblecommon.ProtocolHandler +import io.rebble.libpebblecommon.ProtocolHandlerImpl import io.rebble.libpebblecommon.services.* import io.rebble.libpebblecommon.services.app.AppRunStateService import io.rebble.libpebblecommon.services.appmessage.AppMessageService import io.rebble.libpebblecommon.services.blobdb.BlobDBService -import org.koin.dsl.module import io.rebble.libpebblecommon.services.blobdb.TimelineService import org.koin.dsl.bind +import org.koin.dsl.module val libpebbleModule = module { factory { diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt index 9528a580..923475e9 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/di/StateModule.kt @@ -2,14 +2,12 @@ package io.rebble.cobble.shared.di import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.CurrentToken -import io.rebble.cobble.shared.domain.state.watchOrNull import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import org.koin.core.qualifier.named import org.koin.dsl.bind import org.koin.dsl.module -import kotlin.coroutines.EmptyCoroutineContext val stateModule = module { single(named("connectionState")) { diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt index 0a9693d0..3322a30e 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt @@ -4,9 +4,8 @@ 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.SyncedLockerEntryPlatformImages -import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms -import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Serializable data class LockerEntry( diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/CalendarSync.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/CalendarSync.kt index c82a21ca..6e12e008 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/CalendarSync.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/CalendarSync.kt @@ -1,21 +1,19 @@ package io.rebble.cobble.shared.domain.calendar import io.rebble.cobble.shared.Logging -import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.database.NextSyncAction import io.rebble.cobble.shared.database.dao.CalendarDao import io.rebble.cobble.shared.database.dao.TimelinePinDao import io.rebble.cobble.shared.database.entity.Calendar -import io.rebble.cobble.shared.database.getDatabase import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.domain.common.SystemAppIDs.calendarWatchappId import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.cobble.shared.domain.timeline.WatchTimelineSyncer -import io.rebble.libpebblecommon.packets.WatchVersion -import io.rebble.libpebblecommon.services.blobdb.BlobDBService -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koin.core.qualifier.named diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/PhoneCalendarSyncer.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/PhoneCalendarSyncer.kt index e8e24f0c..8a9ab9d2 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/PhoneCalendarSyncer.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/calendar/PhoneCalendarSyncer.kt @@ -1,14 +1,11 @@ package io.rebble.cobble.shared.domain.calendar -import com.benasher44.uuid.Uuid -import com.benasher44.uuid.uuid4 import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.data.toTimelinePin import io.rebble.cobble.shared.database.NextSyncAction import io.rebble.cobble.shared.database.dao.CalendarDao import io.rebble.cobble.shared.database.dao.TimelinePinDao -import io.rebble.cobble.shared.database.entity.Calendar import io.rebble.cobble.shared.database.entity.isInPast import io.rebble.cobble.shared.datastore.KMPPrefs import io.rebble.cobble.shared.domain.common.SystemAppIDs.calendarWatchappId @@ -20,7 +17,6 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.inject import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.minutes private fun Instant.moveToStartOfDay(): Instant { val localDateTime = toLocalDateTime(TimeZone.currentSystemDefault()) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt index ad0357f9..edabae02 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/common/PebbleDevice.kt @@ -1,12 +1,10 @@ package io.rebble.cobble.shared.domain.common import com.benasher44.uuid.Uuid -import io.ktor.http.parametersOf import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.domain.appmessage.AppMessageTransactionSequence import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager -import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.cobble.shared.handlers.CobbleHandler import io.rebble.cobble.shared.handlers.OutgoingMessage import io.rebble.cobble.shared.middleware.PutBytesController diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationActionHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationActionHandler.kt index 06a9dac0..671053be 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationActionHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/notifications/NotificationActionHandler.kt @@ -1,7 +1,6 @@ package io.rebble.cobble.shared.domain.notifications import io.rebble.cobble.shared.Logging -import io.rebble.cobble.shared.domain.common.SystemAppIDs.notificationsWatchappId import io.rebble.cobble.shared.domain.timeline.TimelineActionManager import io.rebble.cobble.shared.handlers.CobbleHandler import io.rebble.libpebblecommon.services.blobdb.TimelineService diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt index 9afebcbc..0ea96e07 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/state/ConnectionState.kt @@ -1,9 +1,8 @@ package io.rebble.cobble.shared.domain.state import io.rebble.cobble.shared.domain.common.PebbleDevice -import io.rebble.libpebblecommon.packets.WatchVersion -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koin.core.qualifier.named diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/TimelineActionManager.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/TimelineActionManager.kt index 92226d63..1eab2708 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/TimelineActionManager.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/TimelineActionManager.kt @@ -2,8 +2,6 @@ package io.rebble.cobble.shared.domain.timeline import com.benasher44.uuid.Uuid import io.rebble.cobble.shared.Logging -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import io.rebble.cobble.shared.database.dao.TimelinePinDao import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.packets.blobdb.TimelineAction @@ -13,6 +11,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject class TimelineActionManager(private val pebbleDevice: PebbleDevice): KoinComponent { private val timelineDao: TimelinePinDao by inject() diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/WatchTimelineSyncer.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/WatchTimelineSyncer.kt index 614a563b..eb3b21b4 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/WatchTimelineSyncer.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/timeline/WatchTimelineSyncer.kt @@ -16,13 +16,10 @@ import io.rebble.libpebblecommon.services.blobdb.BlobDBService import io.rebble.libpebblecommon.structmapper.SUUID import io.rebble.libpebblecommon.structmapper.StructMapper import kotlinx.datetime.Clock -import kotlinx.datetime.Instant import kotlinx.serialization.json.Json import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import kotlin.math.round import kotlin.random.Random -import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt index 9d0ab5b0..5cab1178 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppInstallHandler.kt @@ -1,17 +1,11 @@ package io.rebble.cobble.shared.handlers import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsChannel import io.ktor.utils.io.ByteReadChannel -import io.ktor.utils.io.read import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.PlatformContext -import io.rebble.cobble.shared.api.RWS import io.rebble.cobble.shared.database.dao.LockerDao import io.rebble.cobble.shared.domain.common.PebbleDevice -import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.middleware.PutBytesController import io.rebble.cobble.shared.util.AppCompatibility.getBestVariant import io.rebble.cobble.shared.util.File @@ -20,9 +14,6 @@ import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import io.rebble.libpebblecommon.packets.AppFetchRequest import io.rebble.libpebblecommon.packets.AppFetchResponse import io.rebble.libpebblecommon.packets.AppFetchResponseStatus -import io.rebble.libpebblecommon.services.AppFetchService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt index 7e9aedd7..95bd9d62 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppMessageHandler.kt @@ -2,15 +2,10 @@ package io.rebble.cobble.shared.handlers import com.benasher44.uuid.Uuid import io.rebble.cobble.shared.Logging -import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.packets.AppCustomizationSetStockAppTitleMessage import io.rebble.libpebblecommon.packets.AppMessage -import io.rebble.libpebblecommon.packets.AppRunStateMessage import io.rebble.libpebblecommon.packets.AppType -import io.rebble.libpebblecommon.protocolhelpers.PebblePacket -import io.rebble.libpebblecommon.services.app.AppRunStateService -import io.rebble.libpebblecommon.services.appmessage.AppMessageService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.* @@ -18,7 +13,6 @@ import kotlinx.coroutines.launch import kotlinx.datetime.Clock import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import org.koin.core.parameter.parametersOf import kotlin.time.Duration.Companion.seconds private data class AppMessageTimestamp(val app: Uuid, val timestamp: Long) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt index a4f7601c..9717c506 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt @@ -3,8 +3,6 @@ package io.rebble.cobble.shared.handlers import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.packets.AppRunStateMessage -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/CalendarActionHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/CalendarActionHandler.kt index 5cfd6b09..338cc88f 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/CalendarActionHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/CalendarActionHandler.kt @@ -2,7 +2,6 @@ package io.rebble.cobble.shared.handlers import com.benasher44.uuid.Uuid import io.rebble.cobble.shared.Logging -import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.database.NextSyncAction import io.rebble.cobble.shared.database.dao.TimelinePinDao import io.rebble.cobble.shared.domain.calendar.CalendarAction @@ -12,10 +11,8 @@ import io.rebble.cobble.shared.domain.common.SystemAppIDs.calendarWatchappId import io.rebble.cobble.shared.domain.timeline.TimelineActionManager import io.rebble.cobble.shared.domain.timeline.WatchTimelineSyncer import io.rebble.libpebblecommon.packets.blobdb.TimelineIcon -import io.rebble.libpebblecommon.packets.blobdb.TimelineItem import io.rebble.libpebblecommon.services.blobdb.TimelineService import io.rebble.libpebblecommon.util.TimelineAttributeFactory -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/PKJSLifecycleHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/PKJSLifecycleHandler.kt index 1730b70e..8ed9fed0 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/PKJSLifecycleHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/PKJSLifecycleHandler.kt @@ -6,8 +6,6 @@ import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.database.dao.LockerDao import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.js.PKJSApp -import io.rebble.cobble.shared.util.File -import io.rebble.libpebblecommon.packets.AppFetchResponseStatus import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt index 37656970..9e816c95 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/SystemHandler.kt @@ -5,13 +5,15 @@ import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager -import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.libpebblecommon.PacketPriority import io.rebble.libpebblecommon.packets.PhoneAppVersion import io.rebble.libpebblecommon.packets.ProtocolCapsFlag import io.rebble.libpebblecommon.packets.TimeMessage -import kotlinx.coroutines.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import kotlinx.datetime.offsetAt diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt index a9f99c0f..61e79cce 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt @@ -11,20 +11,16 @@ import io.rebble.cobble.shared.database.entity.getBestPlatformForDevice import io.rebble.cobble.shared.database.entity.getSdkVersion import io.rebble.cobble.shared.database.entity.getVersion import io.rebble.cobble.shared.domain.api.appstore.toEntity -import io.rebble.cobble.shared.domain.state.ConnectionState import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.domain.state.watchOrNull -import io.rebble.cobble.shared.util.AppCompatibility import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import io.rebble.libpebblecommon.packets.blobdb.AppMetadata import io.rebble.libpebblecommon.packets.blobdb.BlobCommand 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 diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt index becc409a..3837aa55 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.kt @@ -3,7 +3,6 @@ package io.rebble.cobble.shared.js import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent expect object JsRunnerFactory: KoinComponent { diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt index 36615d5c..8d153884 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/js/PKJSApp.kt @@ -5,7 +5,6 @@ import com.benasher44.uuid.uuidFrom import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.domain.common.PebbleDevice -import io.rebble.cobble.shared.domain.state.ConnectionStateManager import io.rebble.cobble.shared.handlers.OutgoingMessage import io.rebble.cobble.shared.handlers.getAppPbwFile import io.rebble.cobble.shared.util.getPbwJsFilePath @@ -18,8 +17,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* -import okio.buffer -import okio.use import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/DeviceLogController.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/DeviceLogController.kt index de8491e0..e5ca0ea9 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/DeviceLogController.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/DeviceLogController.kt @@ -3,7 +3,9 @@ package io.rebble.cobble.shared.middleware import io.rebble.cobble.shared.Logging import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.packets.LogDump -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlin.random.Random diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt index 87a621d2..0e9a9f40 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/middleware/PutBytesController.kt @@ -12,7 +12,6 @@ import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwBlob import io.rebble.libpebblecommon.metadata.pbz.manifest.PbzManifest import io.rebble.libpebblecommon.packets.ObjectType import io.rebble.libpebblecommon.packets.PutBytesAbort -import io.rebble.libpebblecommon.services.PutBytesService import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -21,7 +20,6 @@ import kotlinx.coroutines.sync.withLock import okio.buffer import okio.use import org.koin.core.component.KoinComponent -import org.koin.core.component.inject class PutBytesController(pebbleDevice: PebbleDevice): KoinComponent { private val putBytesService = pebbleDevice.putBytesService diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/Theme.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/Theme.kt index 965f17b8..5498acef 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/Theme.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/Theme.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.SpanStyle @Immutable data class Theme( diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/AppIconContainer.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/AppIconContainer.kt index cf8ead2b..11f67d55 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/AppIconContainer.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/AppIconContainer.kt @@ -2,7 +2,6 @@ package io.rebble.cobble.shared.ui.common import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/TextIcon.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/TextIcon.kt index c9375e61..96f13908 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/TextIcon.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/TextIcon.kt @@ -1,19 +1,16 @@ package io.rebble.cobble.shared.ui.common import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.width import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.unit.dp @Composable fun TextIcon(font: FontFamily, char: Char, contentDescription: String = "Icon", modifier: Modifier = Modifier, tint: Color = LocalContentColor.current) { diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt index 6eea9a5c..2cf47dca 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt @@ -1,14 +1,15 @@ package io.rebble.cobble.shared.ui.view.home -import androidx.compose.foundation.interaction.PressInteraction -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.dp import io.rebble.cobble.shared.ui.common.RebbleIcons import io.rebble.cobble.shared.ui.nav.Routes diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt index 76910179..24984777 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/TestPage.kt @@ -3,10 +3,8 @@ package io.rebble.cobble.shared.ui.view.home import androidx.compose.foundation.layout.Column import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.currentCompositionLocalContext import androidx.compose.runtime.getValue import io.rebble.cobble.shared.database.dao.LockerDao import io.rebble.cobble.shared.domain.state.ConnectionStateManager diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt index 6e09619e..837992bb 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/Locker.kt @@ -1,7 +1,5 @@ package io.rebble.cobble.shared.ui.view.home.locker -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close @@ -12,7 +10,6 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerAppList.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerAppList.kt index a36848aa..e115b113 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerAppList.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerAppList.kt @@ -1,23 +1,18 @@ package io.rebble.cobble.shared.ui.view.home.locker import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.IconButton -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import io.rebble.cobble.shared.database.dao.LockerDao -import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms import io.rebble.cobble.shared.jobs.LockerSyncJob import io.rebble.cobble.shared.ui.common.RebbleIcons import io.rebble.cobble.shared.ui.viewmodel.LockerItemViewModel import io.rebble.cobble.shared.ui.viewmodel.LockerViewModel -import kotlinx.coroutines.Job import org.koin.compose.getKoin import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerItemSheet.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerItemSheet.kt index 8f55940f..6a5057dc 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerItemSheet.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerItemSheet.kt @@ -6,7 +6,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -15,9 +17,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.max import io.rebble.cobble.shared.ui.AppTheme import io.rebble.cobble.shared.ui.common.AppIconContainer import io.rebble.cobble.shared.ui.common.RebbleIcons diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerListItem.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerListItem.kt index 6f88636f..96d846f2 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerListItem.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerListItem.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.material3.* import androidx.compose.runtime.Composable diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceItem.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceItem.kt index 1f013e20..6f7562d9 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceItem.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceItem.kt @@ -4,8 +4,6 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -14,7 +12,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceList.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceList.kt index d3368273..926ebc07 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceList.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/locker/LockerWatchfaceList.kt @@ -1,15 +1,9 @@ package io.rebble.cobble.shared.ui.view.home.locker import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.SmallFloatingActionButton -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 diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt index 0b4cf4f1..c2cae7e7 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerViewModel.kt @@ -7,7 +7,9 @@ import io.rebble.cobble.shared.database.dao.LockerDao import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms import io.rebble.cobble.shared.domain.state.ConnectionStateManager import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.kt index 7d8cc918..8eea87e8 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.kt @@ -1,13 +1,11 @@ package io.rebble.cobble.shared.util -import com.benasher44.uuid.Uuid import io.rebble.cobble.shared.PlatformContext import io.rebble.libpebblecommon.metadata.WatchType import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwManifest import kotlinx.serialization.json.Json import okio.Source -import okio.buffer val json = Json { ignoreUnknownKeys = true diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/di/IosModule.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/di/IosModule.kt index 1ee13fae..dc62b620 100644 --- a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/di/IosModule.kt +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/di/IosModule.kt @@ -4,7 +4,6 @@ import io.rebble.cobble.shared.IOSPlatformContext import io.rebble.cobble.shared.PlatformContext import io.rebble.cobble.shared.datastore.createDataStore import io.rebble.cobble.shared.handlers.* -import org.koin.core.module.dsl.factoryOf import org.koin.core.qualifier.named import org.koin.dsl.module diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt index 16a4db53..03e8561e 100644 --- a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/js/JsRunnerFactory.ios.kt @@ -3,7 +3,6 @@ package io.rebble.cobble.shared.js import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow import org.koin.core.component.KoinComponent actual object JsRunnerFactory: KoinComponent { diff --git a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.ios.kt b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.ios.kt index e02a4fd2..3a29c9f7 100644 --- a/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.ios.kt +++ b/android/shared/src/iosMain/kotlin/io/rebble/cobble/shared/util/AppInstallUtils.ios.kt @@ -1,6 +1,5 @@ package io.rebble.cobble.shared.util -import com.benasher44.uuid.Uuid import io.rebble.cobble.shared.PlatformContext import io.rebble.libpebblecommon.metadata.WatchType import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo From 451ef11c7fbb5b0d9c8bf056d204d2cd976ef2b7 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 7 Oct 2024 17:37:34 +0100 Subject: [PATCH 20/20] only keep n watchfaces on device, add when adding on phone --- .../11.json | 516 ++++++++++++++++++ .../cobble/shared/database/AppDatabase.kt | 5 +- .../cobble/shared/database/dao/LockerDao.kt | 7 + .../database/entity/SyncedLockerEntry.kt | 2 + .../shared/domain/api/appstore/LockerEntry.kt | 5 +- .../shared/handlers/AppRunStateHandler.kt | 8 +- .../cobble/shared/jobs/LockerSyncJob.kt | 25 +- .../ui/viewmodel/LockerItemViewModel.kt | 60 +- 8 files changed, 595 insertions(+), 33 deletions(-) create mode 100644 android/shared/schemas/io.rebble.cobble.shared.database.AppDatabase/11.json diff --git a/android/shared/schemas/io.rebble.cobble.shared.database.AppDatabase/11.json b/android/shared/schemas/io.rebble.cobble.shared.database.AppDatabase/11.json new file mode 100644 index 00000000..b3e0998e --- /dev/null +++ b/android/shared/schemas/io.rebble.cobble.shared.database.AppDatabase/11.json @@ -0,0 +1,516 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "a8e6b06ca2cfc7b8369de76f17efaae6", + "entities": [ + { + "tableName": "Calendar", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `platformId` TEXT NOT NULL, `name` TEXT NOT NULL, `ownerName` TEXT NOT NULL, `ownerId` TEXT NOT NULL, `color` INTEGER NOT NULL, `enabled` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "platformId", + "columnName": "platformId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerName", + "columnName": "ownerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Calendar_platformId", + "unique": true, + "columnNames": [ + "platformId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Calendar_platformId` ON `${TABLE_NAME}` (`platformId`)" + } + ] + }, + { + "tableName": "TimelinePin", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `parentId` TEXT NOT NULL, `backingId` TEXT, `timestamp` INTEGER NOT NULL, `duration` INTEGER, `type` TEXT NOT NULL, `isVisible` INTEGER NOT NULL, `isFloating` INTEGER NOT NULL, `isAllDay` INTEGER NOT NULL, `persistQuickView` INTEGER NOT NULL, `layout` TEXT NOT NULL, `attributesJson` TEXT, `actionsJson` TEXT, `nextSyncAction` TEXT, PRIMARY KEY(`itemId`))", + "fields": [ + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backingId", + "columnName": "backingId", + "affinity": "TEXT" + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVisible", + "columnName": "isVisible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFloating", + "columnName": "isFloating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAllDay", + "columnName": "isAllDay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "persistQuickView", + "columnName": "persistQuickView", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "layout", + "columnName": "layout", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributesJson", + "columnName": "attributesJson", + "affinity": "TEXT" + }, + { + "fieldPath": "actionsJson", + "columnName": "actionsJson", + "affinity": "TEXT" + }, + { + "fieldPath": "nextSyncAction", + "columnName": "nextSyncAction", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "itemId" + ] + } + }, + { + "tableName": "PersistedNotification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sbnKey` TEXT NOT NULL, `packageName` TEXT NOT NULL, `postTime` INTEGER NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL, `groupKey` TEXT, PRIMARY KEY(`sbnKey`))", + "fields": [ + { + "fieldPath": "sbnKey", + "columnName": "sbnKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postTime", + "columnName": "postTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "groupKey", + "columnName": "groupKey", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sbnKey" + ] + } + }, + { + "tableName": "CachedPackageInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `flags` INTEGER NOT NULL, `updated` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updated", + "columnName": "updated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "NotificationChannel", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageId` TEXT NOT NULL, `channelId` TEXT NOT NULL, `name` TEXT, `description` TEXT, `conversationId` TEXT, `shouldNotify` INTEGER NOT NULL, PRIMARY KEY(`packageId`, `channelId`))", + "fields": [ + { + "fieldPath": "packageId", + "columnName": "packageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "channelId", + "columnName": "channelId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "conversationId", + "columnName": "conversationId", + "affinity": "TEXT" + }, + { + "fieldPath": "shouldNotify", + "columnName": "shouldNotify", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageId", + "channelId" + ] + } + }, + { + "tableName": "SyncedLockerEntry", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `uuid` TEXT NOT NULL, `version` TEXT NOT NULL, `title` TEXT NOT NULL, `type` TEXT NOT NULL, `hearts` INTEGER NOT NULL, `developerName` TEXT NOT NULL, `developerId` TEXT, `configurable` INTEGER NOT NULL, `timelineEnabled` INTEGER NOT NULL, `removeLink` TEXT NOT NULL, `shareLink` TEXT NOT NULL, `pbwLink` TEXT NOT NULL, `pbwReleaseId` TEXT NOT NULL, `pbwIconResourceId` INTEGER NOT NULL DEFAULT 0, `nextSyncAction` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, `lastOpened` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hearts", + "columnName": "hearts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "developerName", + "columnName": "developerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "developerId", + "columnName": "developerId", + "affinity": "TEXT" + }, + { + "fieldPath": "configurable", + "columnName": "configurable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timelineEnabled", + "columnName": "timelineEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removeLink", + "columnName": "removeLink", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareLink", + "columnName": "shareLink", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pbwLink", + "columnName": "pbwLink", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pbwReleaseId", + "columnName": "pbwReleaseId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pbwIconResourceId", + "columnName": "pbwIconResourceId", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nextSyncAction", + "columnName": "nextSyncAction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "lastOpened", + "columnName": "lastOpened", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SyncedLockerEntry_uuid", + "unique": true, + "columnNames": [ + "uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SyncedLockerEntry_uuid` ON `${TABLE_NAME}` (`uuid`)" + } + ] + }, + { + "tableName": "SyncedLockerEntryPlatform", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`platformEntryId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `lockerEntryId` TEXT NOT NULL, `sdkVersion` TEXT NOT NULL, `processInfoFlags` INTEGER NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `icon` TEXT, `list` TEXT, `screenshot` TEXT, FOREIGN KEY(`lockerEntryId`) REFERENCES `SyncedLockerEntry`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "platformEntryId", + "columnName": "platformEntryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lockerEntryId", + "columnName": "lockerEntryId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sdkVersion", + "columnName": "sdkVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "processInfoFlags", + "columnName": "processInfoFlags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "images.icon", + "columnName": "icon", + "affinity": "TEXT" + }, + { + "fieldPath": "images.list", + "columnName": "list", + "affinity": "TEXT" + }, + { + "fieldPath": "images.screenshot", + "columnName": "screenshot", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "platformEntryId" + ] + }, + "indices": [ + { + "name": "index_SyncedLockerEntryPlatform_lockerEntryId", + "unique": false, + "columnNames": [ + "lockerEntryId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SyncedLockerEntryPlatform_lockerEntryId` ON `${TABLE_NAME}` (`lockerEntryId`)" + } + ], + "foreignKeys": [ + { + "table": "SyncedLockerEntry", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "lockerEntryId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a8e6b06ca2cfc7b8369de76f17efaae6')" + ] + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt index 3951237b..a1be50ce 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/AppDatabase.kt @@ -19,7 +19,7 @@ import org.koin.mp.KoinPlatformTools SyncedLockerEntry::class, SyncedLockerEntryPlatform::class ], - version = 10, + version = 11, autoMigrations = [ AutoMigration(1, 2), AutoMigration(2, 3), @@ -29,7 +29,8 @@ import org.koin.mp.KoinPlatformTools AutoMigration(6, 7), AutoMigration(7, 8), AutoMigration(8, 9), - AutoMigration(9, 10) + AutoMigration(9, 10), + AutoMigration(10, 11) ] ) @TypeConverters(Converters::class) diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/LockerDao.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/LockerDao.kt index 2f2f606a..637b3a5e 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/LockerDao.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/dao/LockerDao.kt @@ -6,12 +6,16 @@ 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.coroutines.flow.Flow +import kotlinx.datetime.Instant @Dao interface LockerDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertOrReplace(entry: SyncedLockerEntry) + @Update + suspend fun update(entry: SyncedLockerEntry) + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertOrReplacePlatform(platform: SyncedLockerEntryPlatform) @@ -61,4 +65,7 @@ interface LockerDao { @Query("SELECT * FROM SyncedLockerEntry WHERE nextSyncAction = 'Nothing'") suspend fun getSyncedEntries(): List + + @Query("UPDATE SyncedLockerEntry SET lastOpened = :time WHERE uuid = :uuid") + suspend fun updateLastOpened(uuid: String, time: Instant?) } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt index 19310e0b..1472a2c5 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/database/entity/SyncedLockerEntry.kt @@ -4,6 +4,7 @@ import androidx.room.* import io.rebble.cobble.shared.database.NextSyncAction import io.rebble.cobble.shared.util.AppCompatibility import io.rebble.libpebblecommon.metadata.WatchType +import kotlinx.datetime.Instant @Entity( indices = [ @@ -31,6 +32,7 @@ data class SyncedLockerEntry( val nextSyncAction: NextSyncAction, @ColumnInfo(defaultValue = "-1") val order: Int, + val lastOpened: Instant?, ) data class SyncedLockerEntryWithPlatforms( diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt index 3322a30e..28fb66ec 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/domain/api/appstore/LockerEntry.kt @@ -121,8 +121,9 @@ fun LockerEntry.toEntity(): SyncedLockerEntry { pbwLink = pbw?.file ?: error("PBW is null"), pbwReleaseId = pbw.releaseId, pbwIconResourceId = pbw.iconResourceId, - nextSyncAction = NextSyncAction.Upload, - order = -1 + nextSyncAction = if (type == "watchface") NextSyncAction.Ignore else NextSyncAction.Upload, + order = -1, + lastOpened = null ) } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt index 9717c506..447f45a3 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/handlers/AppRunStateHandler.kt @@ -1,15 +1,20 @@ package io.rebble.cobble.shared.handlers import io.rebble.cobble.shared.Logging +import io.rebble.cobble.shared.database.dao.LockerDao import io.rebble.cobble.shared.domain.common.PebbleDevice import io.rebble.libpebblecommon.packets.AppRunStateMessage import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject class AppRunStateHandler( private val pebbleDevice: PebbleDevice -) : CobbleHandler { +) : CobbleHandler, KoinComponent { + private val lockerDao: LockerDao by inject() init { pebbleDevice.negotiationScope.launch { val deviceScope = pebbleDevice.connectionScope.filterNotNull().first() @@ -24,6 +29,7 @@ class AppRunStateHandler( when (message) { is AppRunStateMessage.AppRunStateStart -> { Logging.v("App started: ${message.uuid.get()}") + lockerDao.updateLastOpened(message.uuid.get().toString(), Clock.System.now()) pebbleDevice.currentActiveApp.value = message.uuid.get() } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt index 61e79cce..3b2cdd13 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/jobs/LockerSyncJob.kt @@ -31,7 +31,10 @@ class LockerSyncJob: KoinComponent { suspend fun beginSync(): Boolean { val locker = withContext(Dispatchers.IO) { RWS.appstoreClient?.getLocker() - } ?: return false + } ?: run { + Logging.w("Failed to fetch locker") + return false + } val storedLocker = withContext(Dispatchers.IO) { lockerDao.getAllEntries() } @@ -39,7 +42,7 @@ class LockerSyncJob: KoinComponent { val changedEntries = locker.filter { new -> val newPlat = new.hardwarePlatforms.map { it.toEntity(new.id) } storedLocker.any { old -> - old.entry.id == new.id && (old.entry != new.toEntity() || old.platforms.any { oldPlat -> newPlat.none { newPlat -> oldPlat.dataEqualTo(newPlat) } }) + old.entry.nextSyncAction != NextSyncAction.Ignore && old.entry.id == new.id && (old.entry != new.toEntity() || old.platforms.any { oldPlat -> newPlat.none { newPlat -> oldPlat.dataEqualTo(newPlat) } }) } } val newEntries = locker.filter { new -> storedLocker.none { old -> old.entry.id == new.id } } @@ -49,6 +52,24 @@ class LockerSyncJob: KoinComponent { changedEntries.forEach { lockerDao.clearPlatformsFor(it.id) } + changedEntries.forEach { + val entity = lockerDao.getEntry(it.id) ?: return@forEach + val changed = it.toEntity() + lockerDao.update(entity.entry.copy( + title = changed.title, + hearts = changed.hearts, + developerName = changed.developerName, + developerId = changed.developerId, + configurable = changed.configurable, + timelineEnabled = changed.timelineEnabled, + removeLink = changed.removeLink, + shareLink = changed.shareLink, + pbwLink = changed.pbwLink, + pbwReleaseId = changed.pbwReleaseId, + pbwIconResourceId = changed.pbwIconResourceId, + nextSyncAction = changed.nextSyncAction, + )) + } lockerDao.insertOrReplaceAll(changedEntries.map { it.toEntity() }) lockerDao.insertOrReplaceAllPlatforms(newEntries.flatMap { new -> new.hardwarePlatforms.map { it.toEntity(new.id) } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt index f8c40167..fe8d2099 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/LockerItemViewModel.kt @@ -21,6 +21,7 @@ import io.rebble.cobble.shared.domain.state.watchOrNull import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import io.rebble.libpebblecommon.packets.blobdb.AppMetadata import io.rebble.libpebblecommon.packets.blobdb.BlobCommand +import io.rebble.libpebblecommon.packets.blobdb.BlobResponse import io.rebble.libpebblecommon.structmapper.SUUID import io.rebble.libpebblecommon.structmapper.StructMapper import kotlinx.coroutines.Dispatchers @@ -28,6 +29,7 @@ import kotlinx.coroutines.IO import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant import org.jetbrains.compose.resources.decodeToImageBitmap import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -73,36 +75,40 @@ class LockerItemViewModel(private val httpClient: HttpClient, val entry: SyncedL check(entry.entry.type == "watchface") { "Only watchfaces can be applied" } val watch = ConnectionStateManager.connectionState.value.watchOrNull viewModelScope.launch(Dispatchers.IO) { - if (entry.entry.nextSyncAction == NextSyncAction.Upload && lockerDao.countEntriesWithNextSyncAction(NextSyncAction.Nothing) >= 99) { - Logging.i("Requested watchface isn't in blobdb because there's too many, swapping it out") - val oldest = lockerDao.getSyncedEntries().minByOrNull { it.hearts } ?: run { - Logging.e("No watchfaces to drop") - return@launch - } - + if (entry.entry.nextSyncAction == NextSyncAction.Ignore) { + Logging.i("Watchface is offloaded and never synced, adding to locker") val blobDBService = watch?.blobDBService ?: run { - Logging.e("No connected watch") + Logging.e("No watch connected") return@launch } - - val watchPlatform = WatchHardwarePlatform - .fromProtocolNumber(watch.metadata.value?.running?.hardwarePlatform?.get() ?: 0u) - val platform = entry.getBestPlatformForDevice(watchPlatform?.watchType ?: return@launch) ?: run { - Logging.e("No platform for watch") + val platform = entry.getBestPlatformForDevice(WatchHardwarePlatform.fromProtocolNumber(watch.metadata.value?.running?.hardwarePlatform?.get() ?: 0u)!!.watchType) ?: run { + Logging.e("No platform found for watch") return@launch } + val syncedEntries = lockerDao.getSyncedEntries() + val totalWatchfacesSynced = syncedEntries.count { it.type == "watchface" } + if (totalWatchfacesSynced >= 10) { + val oldest = lockerDao.getSyncedEntries().filter { it.type == "watchface" && it.nextSyncAction == NextSyncAction.Nothing }.minByOrNull { it.lastOpened ?: Instant.DISTANT_PAST } + oldest?.let { + Logging.d("Removing oldest entry ${it.title} ${it.uuid}") + val res = blobDBService.send( + BlobCommand.DeleteCommand( + Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), + BlobCommand.BlobDatabase.App, + SUUID(StructMapper(), uuidFrom(oldest.uuid)).toBytes() + ) + ) + if (res.responseValue != BlobResponse.BlobStatus.Success) { + Logging.e("Failed to delete oldest entry (${res.responseValue})") + } else { + lockerDao.setNextSyncAction(it.id, NextSyncAction.Ignore) + } + } ?: Logging.w("No oldest entry found") + } + Logging.d("Inserting watchface ${entry.entry.uuid}") val (appVersionMajor, appVersionMinor) = entry.getVersion() val (sdkVersionMajor, sdkVersionMinor) = platform.getSdkVersion() - - blobDBService.send( - BlobCommand.DeleteCommand( - Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), - BlobCommand.BlobDatabase.App, - SUUID(StructMapper(), uuidFrom(oldest.uuid)).toBytes() - ) - ) - - blobDBService.send( + val res = blobDBService.send( BlobCommand.InsertCommand( Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), BlobCommand.BlobDatabase.App, @@ -119,9 +125,11 @@ class LockerItemViewModel(private val httpClient: HttpClient, val entry: SyncedL }.toBytes() ) ) - - lockerDao.setNextSyncAction(oldest.id, NextSyncAction.Upload) - lockerDao.setNextSyncAction(entry.entry.id, NextSyncAction.Nothing) + if (res.responseValue == BlobResponse.BlobStatus.Success) { + lockerDao.setNextSyncAction(entry.entry.id, NextSyncAction.Nothing) + } else { + Logging.e("Failed to insert watchface (${res.responseValue})") + } } watch?.appRunStateService?.startApp(uuidFrom(entry.entry.uuid)) }