Android Wear - WIP

pull/3035/head^2
Alex Baker 1 year ago
parent 1112881411
commit ff1f7e1d01

@ -237,7 +237,7 @@ dependencies {
implementation("androidx.compose.material:material")
implementation("androidx.compose.runtime:runtime-livedata")
implementation(libs.androidx.activity.compose)
implementation("androidx.compose.material:material-icons-extended")
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation("androidx.compose.ui:ui-viewbinding")
implementation("androidx.compose.ui:ui-tooling-preview")
@ -261,6 +261,11 @@ dependencies {
googleplayImplementation(libs.play.billing.ktx)
googleplayImplementation(libs.play.review)
googleplayImplementation(libs.play.services.oss.licenses)
googleplayImplementation(libs.horologist.datalayer.phone)
googleplayImplementation(libs.horologist.datalayer.grpc)
googleplayImplementation(libs.horologist.datalayer.core)
googleplayImplementation(libs.play.services.wearable)
googleplayImplementation(projects.wearDatalayer)
androidTestImplementation(libs.dagger.hilt.testing)
kspAndroidTest(libs.dagger.hilt.compiler)

@ -40,6 +40,19 @@
android:name=".location.GoogleGeofenceTransitionIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service
android:name=".wear.WearDataService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="com.google.android.gms.wearable.REQUEST_RECEIVED" />
<data
android:host="*"
android:pathPrefix="/grpc/"
android:scheme="wear" />
</intent-filter>
</service>
</application>
</manifest>

@ -0,0 +1,38 @@
package org.tasks.wear
import androidx.lifecycle.lifecycleScope
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.WearDataLayerRegistry
import com.google.android.horologist.datalayer.grpc.server.BaseGrpcDataService
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskCompleter
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.WearServiceGrpcKt
import org.tasks.preferences.Preferences
import javax.inject.Inject
@OptIn(ExperimentalHorologistApi::class)
@AndroidEntryPoint
class WearDataService : BaseGrpcDataService<WearServiceGrpcKt.WearServiceCoroutineImplBase>() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskCompleter: TaskCompleter
override val registry: WearDataLayerRegistry by lazy {
WearDataLayerRegistry.fromContext(
application = applicationContext,
coroutineScope = lifecycleScope,
).apply {
registerSerializer(TasksSerializer)
}
}
override fun buildService(): WearServiceGrpcKt.WearServiceCoroutineImplBase {
return WearService(
taskDao = taskDao,
preferences = preferences,
taskCompleter = taskCompleter,
)
}
}

@ -0,0 +1,49 @@
package org.tasks.wear
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskCompleter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.tasks.GrpcProto.CompleteTaskRequest
import org.tasks.GrpcProto.CompleteTaskResponse
import org.tasks.GrpcProto.GetTasksRequest
import org.tasks.GrpcProto.Task
import org.tasks.GrpcProto.Tasks
import org.tasks.WearServiceGrpcKt
import org.tasks.filters.MyTasksFilter
import org.tasks.preferences.Preferences
class WearService(
private val taskDao: TaskDao,
private val preferences: Preferences,
private val taskCompleter: TaskCompleter,
) : WearServiceGrpcKt.WearServiceCoroutineImplBase() {
override suspend fun getTasks(request: GetTasksRequest): Tasks {
return Tasks.newBuilder()
.addAllTasks(getTasks())
.build()
}
override suspend fun completeTask(request: CompleteTaskRequest): CompleteTaskResponse {
taskCompleter.setComplete(request.id, request.completed)
return CompleteTaskResponse.newBuilder().setSuccess(true).build()
}
private suspend fun getTasks(): List<Task> = withContext(Dispatchers.IO) {
val tasks = taskDao.fetchTasks(preferences, MyTasksFilter.create())
return@withContext tasks.map {
Task.newBuilder()
.setId(it.task.id)
.setPriority(it.task.priority)
.setCompleted(it.task.isCompleted)
.apply {
if (it.task.title != null) {
setTitle(it.task.title)
}
}
.setRepeating(it.task.isRecurring)
.build()
}
}
}

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="android_wear_capabilities">
<item>data_layer_app_helper_device_phone</item>
<item>horologist_phone</item>
</string-array>
</resources>

@ -33,10 +33,10 @@ class TaskCompleter @Inject internal constructor(
private val gCalHelper: GCalHelper,
private val workManager: WorkManager,
) {
suspend fun setComplete(taskId: Long) =
suspend fun setComplete(taskId: Long, completed: Boolean = true) =
taskDao
.fetch(taskId)
?.let { setComplete(it, true) }
?.let { setComplete(it, completed) }
?: Timber.e("Could not find task $taskId")
suspend fun setComplete(item: Task, completed: Boolean, includeChildren: Boolean = true) {

@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.protobuf) apply false
}
buildscript {

@ -1,4 +1,5 @@
[versions]
agp = "8.7.0"
android-compileSdk = "34"
android-minSdk = "24"
android-targetSdk = "34"
@ -24,8 +25,9 @@ google-oauth2 = "1.24.0"
google-api-drive = "v3-rev20240914-2.0.0"
google-api-tasks = "v1-rev20240630-2.0.0"
google-services = "4.4.2"
agp = "8.7.0"
grpc = "1.68.0"
hilt = "1.2.0"
horologist = "0.6.20"
ical4android = "12fe73a"
jchronic = "0.2.6"
jems = "1.33"
@ -50,6 +52,7 @@ play-services-maps = "19.0.0"
play-services-location = "21.3.0"
play-services-oss-licenses = "17.1.0"
preference = "1.2.1"
protobuf = "4.28.2"
recyclerview = "1.3.2"
retrofit = "2.9.0"
rfc5545-datetime = "0.2.4"
@ -62,6 +65,7 @@ work = "2.8.1"
androidx-test = "1.6.1"
androidx-test-runner = "1.6.2"
xpp3 = "1.1.6"
wearCompose = "1.4.0"
[libraries]
accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" }
@ -72,6 +76,7 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a
androidx-compose = { module = "androidx.compose:compose-bom", version.ref = "compose" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.0.1" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version = "1.1.1" }
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment-ktx" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt" }
@ -81,6 +86,7 @@ androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime",
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
androidx-room = { module = "androidx.room:room-runtime", version.ref = "room" }
@ -120,6 +126,13 @@ google-api-drive = { module = "com.google.apis:google-api-services-drive", versi
google-api-tasks = { module = "com.google.apis:google-api-services-tasks", version.ref = "google-api-tasks" }
google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" }
horologist-compose-layout = { group = "com.google.android.horologist", name = "horologist-compose-layout", version.ref = "horologist" }
horologist-compose-material = { group = "com.google.android.horologist", name = "horologist-compose-material", version.ref = "horologist" }
horologist-compose-tools = { group = "com.google.android.horologist", name = "horologist-compose-tools", version.ref = "horologist" }
horologist-datalayer-core = { group = "com.google.android.horologist", name = "horologist-datalayer", version.ref = "horologist" }
horologist-datalayer-grpc = { group = "com.google.android.horologist", name = "horologist-datalayer-grpc", version.ref = "horologist" }
horologist-datalayer-phone = { group = "com.google.android.horologist", name = "horologist-datalayer-phone", version.ref = "horologist" }
horologist-datalayer-watch = { group = "com.google.android.horologist", name = "horologist-datalayer-watch", version.ref = "horologist" }
jchronic = { module = "com.rubiconproject.oss:jchronic", version.ref = "jchronic" }
junit = { module = "junit:junit", version.ref = "junit-junit" }
kermit = { module = "co.touchlab:kermit", version = "2.0.4" }
@ -159,6 +172,27 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
xpp3 = { module = "org.ogce:xpp3", version.ref = "xpp3" }
androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version = "1.7.2" }
iconics = { module = "com.mikepenz:iconics-core", version = "5.5.0-b01" }
play-services-wearable = { group = "com.google.android.gms", name = "play-services-wearable", version = "18.2.0" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
wear-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "wearCompose" }
wear-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "wearCompose" }
wear-compose-navigation = { group = "androidx.wear.compose", name = "compose-navigation", version.ref = "wearCompose" }
wear-tooling-preview = { group = "androidx.wear", name = "wear-tooling-preview", version = "1.0.0" }
protobuf-protoc-gen-grpc-java = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" }
protobuf-protoc-gen-grpc-kotlin = { module = "io.grpc:protoc-gen-grpc-kotlin", version = "1.3.0:jdk8@jar" }
protobuf-protoc-gen-javalite = { group = "com.google.protobuf", name = "protoc-gen-javalite", version = "3.0.0" }
protobuf-protoc-stnd = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
io-grpc-grpc-android = { group = "io.grpc", name = "grpc-android", version.ref = "grpc" }
io-grpc-grpc-binder = { group = "io.grpc", name = "grpc-binder", version.ref = "grpc" }
io-grpc-grpc-kotlin = { group = "io.grpc", name = "grpc-kotlin-stub", version = "1.4.1" }
io-grpc-protobuf-lite = { group = "io.grpc", name = "grpc-protobuf-lite", version.ref = "grpc" }
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@ -171,3 +205,4 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
ksp = { id = "com.google.devtools.ksp", version = "2.0.10-1.0.24" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
protobuf = { id = "com.google.protobuf", version = "0.9.4" }

@ -44,3 +44,5 @@ include("app")
include("data")
include(":kmp")
include(":icons")
include(":wear")
include(":wear-datalayer")

@ -0,0 +1 @@
/build

@ -0,0 +1,88 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.jetbrains.kotlin.android)
id("com.google.protobuf")
}
android {
namespace = "org.tasks.wear"
compileSdk = 34
defaultConfig {
minSdk = 24
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_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
val archSuffix = if (System.getProperty("os.name").contains("mac", ignoreCase = true))
":osx-x86_64"
else
""
protobuf {
protoc {
artifact = libs.protobuf.protoc.stnd.get().toString() + archSuffix
}
plugins {
create("javalite") {
artifact = libs.protobuf.protoc.gen.javalite.get().toString() + archSuffix
}
create("grpc") {
artifact = libs.protobuf.protoc.gen.grpc.java.get().toString()
}
create("grpckt") {
artifact = libs.protobuf.protoc.gen.grpc.kotlin.get().toString()
}
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
create("kotlin") {
option("lite")
}
}
task.plugins {
create("grpc") {
option("lite")
}
create("grpckt") {
option("lite")
}
}
}
}
}
dependencies {
api(libs.io.grpc.grpc.kotlin)
api(libs.io.grpc.protobuf.lite)
api(libs.io.grpc.grpc.android)
api(libs.io.grpc.grpc.binder)
implementation(libs.horologist.datalayer.core)
implementation(libs.androidx.datastore)
implementation(libs.protobuf.kotlin.lite)
}

@ -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

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

@ -0,0 +1,24 @@
package org.tasks.wear
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.google.protobuf.InvalidProtocolBufferException
import org.tasks.GrpcProto.Tasks
import java.io.InputStream
import java.io.OutputStream
object TasksSerializer : Serializer<Tasks> {
override val defaultValue: Tasks
get() = Tasks.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Tasks =
try {
Tasks.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
override suspend fun writeTo(t: Tasks, output: OutputStream) {
t.writeTo(output)
}
}

@ -0,0 +1,30 @@
syntax = "proto3";
package org.tasks.grpc;
option java_package = "org.tasks";
option java_outer_classname = "GrpcProto";
message Task {
uint64 id = 1;
string title = 2;
bool completed = 3;
uint32 priority = 4;
bool repeating = 5;
}
message Tasks {
repeated Task tasks = 1;
}
message GetTasksRequest {}
message CompleteTaskRequest {
uint64 id = 1;
bool completed = 2;
}
message CompleteTaskResponse { bool success = 1; }
service WearService {
rpc getTasks(GetTasksRequest) returns (Tasks);
rpc completeTask(CompleteTaskRequest) returns (CompleteTaskResponse);
}

1
wear/.gitignore vendored

@ -0,0 +1 @@
/build

@ -0,0 +1,69 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.kotlin.compose.compiler)
}
android {
namespace = "org.tasks"
compileSdk = 34
defaultConfig {
applicationId = "org.tasks"
minSdk = 30
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
tasks.register("testClasses")
}
dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(projects.wearDatalayer)
implementation(projects.kmp)
implementation(libs.play.services.wearable)
implementation(platform(libs.androidx.compose))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material.icons.extended)
implementation(libs.wear.compose.material)
implementation(libs.wear.compose.foundation)
implementation(libs.wear.compose.navigation)
implementation(libs.wear.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.core.splashscreen)
implementation(libs.horologist.compose.layout)
implementation(libs.horologist.compose.material)
implementation(libs.horologist.compose.tools)
implementation(libs.horologist.datalayer.watch)
implementation(libs.horologist.datalayer.core)
implementation(libs.horologist.datalayer.grpc)
androidTestImplementation(platform(libs.androidx.compose))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Ignore the IconLocation for the Tile preview images -->
<issue id="IconLocation">
<ignore path="res/drawable/tile_preview.png" />
<ignore path="res/drawable-round/tile_preview.png" />
</issue>
</lint>

@ -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

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-feature android:name="android.hardware.type.watch" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<uses-library
android:name="com.google.android.wearable"
android:required="true" />
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="false" />
<activity
android:name=".presentation.MainActivity"
android:exported="true"
android:taskAffinity=""
android:theme="@style/MainActivityTheme.Starting">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

@ -0,0 +1,112 @@
/* While this template provides a good starting point for using Wear Compose, you can always
* take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter to find the
* most up to date changes to the libraries and their usages.
*/
package org.tasks.presentation
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.TimeText
import androidx.wear.compose.navigation.SwipeDismissableNavHost
import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
import androidx.wear.tooling.preview.devices.WearDevices
import com.google.android.horologist.compose.layout.AppScaffold
import org.tasks.R
import org.tasks.presentation.screens.TaskListScreen
import org.tasks.presentation.screens.TaskListViewModel
import org.tasks.presentation.theme.TasksTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setTheme(android.R.style.Theme_DeviceDefault)
setContent {
TasksTheme {
AppScaffold(
timeText = { TimeText() },
modifier = Modifier.background(MaterialTheme.colors.background),
) {
val navController = rememberSwipeDismissableNavController()
SwipeDismissableNavHost(
startDestination = "task_list",
navController = navController,
) {
composable("task_list") { navBackStackEntry ->
val viewModel: TaskListViewModel = viewModel(navBackStackEntry)
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
TaskListScreen(
tasks = uiState.tasks.tasksList,
onComplete = { viewModel.completeTask(it) },
onClick = { navController.navigate("task_edit/$it") },
)
}
composable(
route = "task_edit/{taskId}",
arguments = listOf(
navArgument("taskId") { type = NavType.StringType }
)
) {
val taskId = it.arguments?.getString("taskId")
WearApp(taskId ?: "invalid id")
}
}
}
}
}
}
}
@Composable
fun WearApp(greetingName: String) {
TasksTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background),
contentAlignment = Alignment.Center
) {
TimeText()
Greeting(greetingName = greetingName)
}
}
}
@Composable
fun Greeting(greetingName: String) {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
color = MaterialTheme.colors.primary,
text = stringResource(R.string.hello_world, greetingName)
)
}
@Preview(device = WearDevices.SMALL_ROUND, showSystemUi = true)
@Composable
fun DefaultPreview() {
WearApp("Preview Android")
}

@ -0,0 +1,4 @@
package org.tasks.presentation
class MainActivityViewModel {
}

@ -0,0 +1,94 @@
package org.tasks.presentation.screens
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CheckBox
import androidx.compose.material.icons.outlined.CheckBoxOutlineBlank
import androidx.compose.material.icons.outlined.Repeat
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Card
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScreenScaffold
import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
import org.tasks.GrpcProto
import org.tasks.kmp.org.tasks.themes.ColorProvider
@OptIn(ExperimentalHorologistApi::class)
@Composable
fun TaskListScreen(
tasks: MutableList<GrpcProto.Task>,
onComplete: (Long) -> Unit,
onClick: (Long) -> Unit,
) {
val columnState = rememberResponsiveColumnState()
ScreenScaffold(scrollState = columnState) {
ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
columnState = columnState,
) {
items(tasks.size) { index ->
val task = tasks[index]
key(task.id) {
TaskCard(
task = task,
onComplete = { onComplete(task.id) },
onClick = { onClick(task.id) },
)
}
}
}
}
}
@Composable
fun TaskCard(
task: GrpcProto.Task,
onComplete: () -> Unit,
onClick: () -> Unit,
) {
Card(
onClick = onClick,
backgroundPainter = ColorPainter(MaterialTheme.colors.surface),
contentPadding = PaddingValues(start = 0.dp, top = 0.dp, end = 12.dp, bottom = 0.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Button(
onClick = onComplete,
colors = ButtonDefaults.iconButtonColors(),
) {
Icon(
imageVector = when {
task.completed -> Icons.Outlined.CheckBox
task.repeating -> Icons.Outlined.Repeat
else -> Icons.Outlined.CheckBoxOutlineBlank
},
tint = Color(
ColorProvider.priorityColor(task.priority, isDarkMode = true, desaturate = true)
),
contentDescription = null,
)
}
Text(
text = task.title,
modifier = Modifier.padding(vertical = 12.dp)
)
}
}
}

@ -0,0 +1,54 @@
package org.tasks.presentation.screens
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.TargetNodeId
import com.google.android.horologist.data.WearDataLayerRegistry
import com.google.android.horologist.datalayer.grpc.GrpcExtensions.grpcClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.tasks.GrpcProto
import org.tasks.GrpcProto.Tasks
import org.tasks.WearServiceGrpcKt
import org.tasks.wear.TasksSerializer
data class TaskListScreenState(
val error: String? = null,
val tasks: Tasks = Tasks.getDefaultInstance(),
)
@OptIn(ExperimentalHorologistApi::class)
class TaskListViewModel(
application: Application
) : AndroidViewModel(application) {
val uiState: MutableStateFlow<TaskListScreenState> = MutableStateFlow(TaskListScreenState())
private val scope = CoroutineScope(Dispatchers.IO)
private val wearDataLayerRegistry = WearDataLayerRegistry.fromContext(
application = application,
coroutineScope = scope,
).apply {
registerSerializer(TasksSerializer)
}
private val wearService : WearServiceGrpcKt.WearServiceCoroutineStub = wearDataLayerRegistry.grpcClient(
nodeId = TargetNodeId.PairedPhone,
coroutineScope = scope,
) {
WearServiceGrpcKt.WearServiceCoroutineStub(it)
}
init {
viewModelScope.launch {
val tasks = wearService.getTasks(GrpcProto.GetTasksRequest.getDefaultInstance())
uiState.update { it.copy(tasks = tasks) }
}
}
fun completeTask(it: Long) = viewModelScope.launch {
wearService.completeTask(GrpcProto.CompleteTaskRequest.newBuilder().setId(it).setCompleted(true).build())
}
}

@ -0,0 +1,17 @@
package org.tasks.presentation.theme
import androidx.compose.runtime.Composable
import androidx.wear.compose.material.MaterialTheme
@Composable
fun TasksTheme(
content: @Composable () -> Unit
) {
/**
* Empty theme to customize for your app.
* See: https://developer.android.com/jetpack/compose/designsystems/custom
*/
MaterialTheme(
content = content
)
}

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/blue_500" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_no_shadow_foreground" />
</adaptive-icon>

@ -0,0 +1,3 @@
<resources>
<string name="hello_world">From the Round world,\nHello, %1$s!</string>
</resources>

@ -0,0 +1,8 @@
<resources>
<string name="app_name">Tasks.org</string>
<!--
This string is used for square devices and overridden by hello_world in
values-round/strings.xml for round devices.
-->
<string name="hello_world">From the Square world,\nHello, %1$s!</string>
</resources>

@ -0,0 +1,8 @@
<resources>
<style name="MainActivityTheme.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@android:color/black</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_no_shadow_foreground</item>
<item name="postSplashScreenTheme">@android:style/Theme.DeviceDefault</item>
</style>
</resources>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="android_wear_capabilities">
<item>data_layer_app_helper_device_watch</item>
<item>horologist_watch</item>
</string-array>
</resources>
Loading…
Cancel
Save