Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ android {
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation("com.google.android.gms:play-services-wearable:20.0.1")

def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class MainApplication : Application(), ReactApplication {
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
add(SharedStoragePackage())
add(WearSyncPackage())
}

override fun getJSMainModuleName(): String = "index"
Expand Down
55 changes: 55 additions & 0 deletions android/app/src/main/java/com/colonelpanic/mova/WearSyncModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.colonelpanic.mova

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.google.android.gms.wearable.PutDataMapRequest
import com.google.android.gms.wearable.Wearable

private const val CONFIG_PATH = "/mova/config"

class WearSyncModule(
private val reactContext: ReactApplicationContext,
) : ReactContextBaseJavaModule(reactContext) {

override fun getName(): String = "WearSync"

@ReactMethod
fun syncCredentials(
apiUrl: String,
username: String,
password: String,
promise: Promise,
) {
val request = PutDataMapRequest.create(CONFIG_PATH).apply {
dataMap.putBoolean("configured", true)
dataMap.putString("apiUrl", apiUrl)
dataMap.putString("username", username)
dataMap.putString("password", password)
dataMap.putLong("updatedAt", System.currentTimeMillis())
}.asPutDataRequest().setUrgent()

Wearable.getDataClient(reactContext)
.putDataItem(request)
.addOnSuccessListener { promise.resolve(null) }
.addOnFailureListener { error ->
promise.reject("WEAR_SYNC_FAILED", error)
}
}

@ReactMethod
fun clearCredentials(promise: Promise) {
val request = PutDataMapRequest.create(CONFIG_PATH).apply {
dataMap.putBoolean("configured", false)
dataMap.putLong("updatedAt", System.currentTimeMillis())
}.asPutDataRequest().setUrgent()

Wearable.getDataClient(reactContext)
.putDataItem(request)
.addOnSuccessListener { promise.resolve(null) }
.addOnFailureListener { error ->
promise.reject("WEAR_SYNC_FAILED", error)
}
}
}
16 changes: 16 additions & 0 deletions android/app/src/main/java/com/colonelpanic/mova/WearSyncPackage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.colonelpanic.mova

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class WearSyncPackage : ReactPackage {
override fun createNativeModules(
reactContext: ReactApplicationContext,
): MutableList<NativeModule> = mutableListOf(WearSyncModule(reactContext))

override fun createViewManagers(
reactContext: ReactApplicationContext,
): MutableList<ViewManager<*, *>> = mutableListOf()
}
1 change: 1 addition & 0 deletions android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ rootProject.name = 'mova'
expoAutolinking.useExpoVersionCatalog()

include ':app'
include ':wear'
includeBuild(expoAutolinking.reactNativeGradlePlugin)

include ':detox'
Expand Down
41 changes: 41 additions & 0 deletions android/wear/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"

def packageJson = new groovy.json.JsonSlurper().parse(file("../../package.json"))

android {
namespace "com.colonelpanic.mova.wear"
compileSdk rootProject.ext.compileSdkVersion

defaultConfig {
applicationId "com.colonelpanic.mova"
minSdkVersion 26
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName packageJson.version
}

signingConfigs {
debug {
storeFile file("../app/debug.keystore")
storePassword "android"
keyAlias "androiddebugkey"
keyPassword "android"
}
}

buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
signingConfig signingConfigs.debug
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
}

dependencies {
implementation("com.google.android.gms:play-services-wearable:20.0.1")
}
2 changes: 2 additions & 0 deletions android/wear/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Keep the Wear module small for now. Add rules here if release minification is
# enabled later.
31 changes: 31 additions & 0 deletions android/wear/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.type.watch" android:required="true"/>
<uses-permission android:name="android.permission.INTERNET"/>

<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true"/>

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

<service
android:name=".ConfigListenerService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.wearable.DATA_CHANGED"/>
</intent-filter>
</service>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.colonelpanic.mova.wear

import android.util.Base64
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL

data class CaptureResult(
val success: Boolean,
val message: String,
)

object CaptureClient {
fun capture(credentials: WearCredentials, text: String): CaptureResult {
val connection = try {
URL("${credentials.apiUrl}/capture").openConnection() as HttpURLConnection
} catch (error: Exception) {
return CaptureResult(false, "Invalid server URL")
}

return try {
connection.requestMethod = "POST"
connection.connectTimeout = 8000
connection.readTimeout = 12000
connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("Authorization", credentials.authHeader())

val body = JSONObject()
.put("template", "default")
.put(
"values",
JSONObject().put("Title", text),
)
.toString()

connection.outputStream.use { stream ->
stream.write(body.toByteArray(Charsets.UTF_8))
}

when (val responseCode = connection.responseCode) {
in 200..299 -> CaptureResult(true, "Captured")
401 -> CaptureResult(false, "Authentication failed")
else -> CaptureResult(false, "Server error $responseCode")
}
} catch (error: Exception) {
CaptureResult(false, "Network error")
} finally {
connection.disconnect()
}
}

private fun WearCredentials.authHeader(): String {
val token = "$username:$password"
return "Basic " + Base64.encodeToString(token.toByteArray(), Base64.NO_WRAP)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.colonelpanic.mova.wear

import com.google.android.gms.wearable.DataEvent
import com.google.android.gms.wearable.DataEventBuffer
import com.google.android.gms.wearable.DataMapItem
import com.google.android.gms.wearable.WearableListenerService

class ConfigListenerService : WearableListenerService() {
override fun onDataChanged(dataEvents: DataEventBuffer) {
try {
dataEvents
.filter { event ->
event.type == DataEvent.TYPE_CHANGED &&
event.dataItem.uri.path == CONFIG_PATH
}
.forEach { event ->
val dataMap = DataMapItem.fromDataItem(event.dataItem).dataMap
val configured = dataMap.getBoolean("configured", false)

if (!configured) {
MovaWearStorage.clearCredentials(this)
return@forEach
}

val apiUrl = dataMap.getString("apiUrl")
val username = dataMap.getString("username")
val password = dataMap.getString("password")

if (!apiUrl.isNullOrBlank() && !username.isNullOrBlank() && password != null) {
MovaWearStorage.saveCredentials(this, apiUrl, username, password)
}
}
} finally {
dataEvents.release()
}
}
}
Loading
Loading