diff --git a/Complications/.google/packaging.yaml b/Complications/.google/packaging.yaml new file mode 100644 index 000000000..f9f6c656e --- /dev/null +++ b/Complications/.google/packaging.yaml @@ -0,0 +1,34 @@ +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# GOOGLE SAMPLE PACKAGING DATA +# +# This file is used by Google as part of our samples packaging process. +# End users may safely ignore this file. It has no relevance to other systems. +--- +status: PUBLISHED +technologies: [Android] +categories: [Wearable] +languages: [Java] +solutions: [Mobile] +github: android/wear-os +level: INTERMEDIATE +icon: screenshots/icon-web.png +apiRefs: + - android:android.support.wearable.complications.ComplicationData + - android:android.support.wearable.complications.ComplicationManager + - android:android.support.wearable.complications.ComplicationProviderService + - android:android.support.wearable.complications.ComplicationText + - android:android.support.wearable.complications.ProviderUpdateRequester +license: apache2 diff --git a/Complications/README.md b/Complications/README.md new file mode 100644 index 000000000..f9f8bd5a0 --- /dev/null +++ b/Complications/README.md @@ -0,0 +1,54 @@ + +Android Complications Sample +============================ + +Complication Test Suite is a set of complication data sources that provide dummy data and it can be +used to test how different types of complications render on a watch face. + +Introduction +------------ + +Steps for trying out the sample: +* Compile and install the wearable app onto your Wear device or emulator (for Wear scenario). + +* This sample does not have a main Activity (just Services that provide the complication data). +Therefore, you may see an error next to the 'Run' button. To fix, click on the +"Wearable" dropdown next to the 'Run' button and select 'Edit Configurations'. Under the +'Launch Options', change the 'Launch' field from 'Default APK' to 'Nothing' and save. + +This sample provides dummy data for testing the complications UI in your watch face. After +selecting a type from your watch face configuration Activity, you can tap on the complications to +see more options. + +The Wear app demonstrates the use of [ComplicationData][1], [ComplicationDataSourceService][2], and [ComplicationText][3]. + +[1]: https://developer.android.com/reference/kotlin/androidx/wear/complications/data/ComplicationData +[2]: https://developer.android.com/reference/kotlin/androidx/wear/complications/datasource/ComplicationDataSourceService +[3]: https://developer.android.com/reference/kotlin/androidx/wear/complications/data/ComplicationText + +Pre-requisites +-------------- + +- Android SDK 30 + +Screenshots +------------- + +Screenshot Screenshot + +Getting Started +--------------- + +This sample uses the Gradle build system. To build this project, use the +"gradlew build" command or use "Import Project" in Android Studio. + +Support +------- + +- Stack Overflow: https://stackoverflow.com/questions/tagged/wear-os + +If you've found an error in this sample, please file an issue: +https://github.com/android/wear-os-samples/issues + +Patches are encouraged, and may be submitted by forking this project and +submitting a pull request through GitHub. Please see CONTRIBUTING.md for more details. diff --git a/Complications/Wearable/build.gradle b/Complications/Wearable/build.gradle new file mode 100644 index 000000000..3ae92a443 --- /dev/null +++ b/Complications/Wearable/build.gradle @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.plugin.parcelize' +} + +android { + compileSdk 34 + + namespace "com.example.android.wearable.wear.complications" + + defaultConfig { + versionCode 1 + versionName "1.0" + minSdk 26 + targetSdk 33 + } + + lintOptions { + warningsAsErrors false + } + + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.majorVersion + } + + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation libs.kotlinx.coroutines.android + implementation libs.androidx.core.ktx + implementation libs.androidx.datastore.preferences + implementation libs.androidx.wear.watchface.complications.data.source.ktx +} diff --git a/Complications/Wearable/proguard-rules.pro b/Complications/Wearable/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/Complications/Wearable/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 diff --git a/Complications/Wearable/src/main/AndroidManifest.xml b/Complications/Wearable/src/main/AndroidManifest.xml new file mode 100644 index 000000000..5ceeb8c3f --- /dev/null +++ b/Complications/Wearable/src/main/AndroidManifest.xml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/Complication.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/Complication.kt new file mode 100644 index 000000000..ace9864ab --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/Complication.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +/** + * The enumerated list of all complications provided by this app. + */ +enum class Complication( + + /** + * A stable key that can be used to distinguish between this app's complications. + */ + val key: String, +) { + GOAL_PROGRESS("GoalProgress"), + ICON("Icon"), + LARGE_IMAGE("LargeImage"), + LONG_TEXT("LongText"), + RANGED_VALUE("RangedValue"), + SHORT_TEXT("ShortText"), + SMALL_IMAGE("SmallImage"), + WEIGHTED_ELEMENTS("WeightedElements"), +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/ComplicationToggleArgs.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/ComplicationToggleArgs.kt new file mode 100644 index 000000000..f21f65f32 --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/ComplicationToggleArgs.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.content.ComponentName +import android.os.Parcelable +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.longPreferencesKey +import kotlinx.parcelize.Parcelize + +/** + * The arguments for toggling a complication. + */ +@Parcelize +data class ComplicationToggleArgs( + + /** + * The component of the complication being toggled. + */ + val providerComponent: ComponentName, + + /** + * An app-defined key for different provided complications. + */ + val complication: Complication, + + /** + * The system-defined key for the instance of a provided complication. + * (it's entirely possible for the same complication to be used multiple times) + */ + val complicationInstanceId: Int, +) : Parcelable + +/** + * Returns the key for the preference used to hold the current state of a given complication. + */ +fun ComplicationToggleArgs.getStatePreferenceKey(): Preferences.Key = + longPreferencesKey("${complication.key}_$complicationInstanceId") diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/ComplicationToggleReceiver.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/ComplicationToggleReceiver.kt new file mode 100644 index 000000000..f07acf6c4 --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/ComplicationToggleReceiver.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.os.BundleCompat +import androidx.wear.watchface.complications.datasource.ComplicationDataSourceUpdateRequester +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch + +/** + * Receives intents on tap and causes complication states to be toggled and updated. + */ +class ComplicationToggleReceiver : BroadcastReceiver() { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + + override fun onReceive(context: Context, intent: Intent) { + val args = intent.getArgs() + + val result = goAsync() + + scope.launch { + try { + args.updateState(context) + + // Request an update for the complication that has just been toggled. + ComplicationDataSourceUpdateRequester + .create( + context = context, + complicationDataSourceComponent = args.providerComponent, + ) + .requestUpdate(args.complicationInstanceId) + } finally { + // Always call finish, even if cancelled + result.finish() + } + } + } + + companion object { + private const val EXTRA_ARGS = "arguments" + + /** + * Returns a pending intent, suitable for use as a tap intent, that causes a complication to be + * toggled and updated. + */ + fun getComplicationToggleIntent( + context: Context, + args: ComplicationToggleArgs, + ): PendingIntent { + val intent = Intent(context, ComplicationToggleReceiver::class.java).apply { + putExtra(EXTRA_ARGS, args) + } + // Pass complicationId as the requestCode to ensure that different complications get + // different intents. + return PendingIntent.getBroadcast( + context, + args.complicationInstanceId, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } + + /** + * Returns the [ComplicationToggleArgs] from the [Intent] sent to the [ComplicationToggleArgs]. + */ + private fun Intent.getArgs(): ComplicationToggleArgs = requireNotNull( + BundleCompat.getParcelable( + this.extras!!, + EXTRA_ARGS, + ComplicationToggleArgs::class.java, + ), + ) + } +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/DataStorage.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/DataStorage.kt new file mode 100644 index 000000000..4b544a983 --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/DataStorage.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +val Context.dataStore: DataStore by preferencesDataStore( + name = "Complications", +) + +/** + * Returns the current state for a given complication. + */ +suspend fun ComplicationToggleArgs.getState(context: Context): Long { + val stateKey = getStatePreferenceKey() + return context.dataStore.data + .map { preferences -> + preferences[stateKey] ?: 0 + } + .first() +} + +/** + * Updates the current state for a given complication, incrementing it by 1. + */ +suspend fun ComplicationToggleArgs.updateState(context: Context) { + val stateKey = getStatePreferenceKey() + context.dataStore.edit { preferences -> + val currentValue = preferences[stateKey] ?: 0 + // benign overflow possible, all samples take a modulo of this number + preferences[stateKey] = currentValue + 1 + } +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/GoalProgressDataSourceService.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/GoalProgressDataSourceService.kt new file mode 100644 index 000000000..f39d0acd6 --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/GoalProgressDataSourceService.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.app.PendingIntent +import android.content.ComponentName +import android.graphics.drawable.Icon +import androidx.annotation.RequiresApi +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationText +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.GoalProgressComplicationData +import androidx.wear.watchface.complications.data.MonochromaticImage +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService +import kotlin.random.Random + +/** + * A complication provider that supports only [ComplicationType.GOAL_PROGRESS] and cycles + * through the possible configurations on tap. The value is randomised on each update. + */ +@RequiresApi(33) +class GoalProgressDataSourceService : SuspendingComplicationDataSourceService() { + + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + if (request.complicationType != ComplicationType.GOAL_PROGRESS) { + return NoDataComplicationData() + } + val args = ComplicationToggleArgs( + providerComponent = ComponentName(this, javaClass), + complication = Complication.GOAL_PROGRESS, + complicationInstanceId = request.complicationInstanceId, + ) + val complicationTogglePendingIntent = + ComplicationToggleReceiver.getComplicationToggleIntent( + context = this, + args = args, + ) + // Suspending function to retrieve the complication's state + val state = args.getState(this) + val case = Case.entries[state.mod(Case.entries.size)] + + return getComplicationData( + tapAction = complicationTogglePendingIntent, + case = case, + ) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData? = + getComplicationData( + tapAction = null, + case = Case.TEXT_WITH_ICON, + ) + + private fun getComplicationData( + tapAction: PendingIntent?, + case: Case, + ): ComplicationData { + val text: ComplicationText? + val monochromaticImage: MonochromaticImage? + val title: ComplicationText? + val caseContentDescription: String + + // Create a ceiling above the target value, as GOAL_PROGRESS complication data can exceed + // the target value. + val ceiling = case.targetValue + 5000.0 + val currentValue = Random.nextDouble(0.0, ceiling).toFloat() + val percentage = currentValue / case.targetValue + + when (case) { + Case.TEXT_ONLY -> { + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_only), + ).build() + monochromaticImage = null + title = null + caseContentDescription = getString( + R.string.goal_progress_text_only_content_description, + ) + } + Case.TEXT_WITH_ICON -> { + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_icon), + ).build() + monochromaticImage = MonochromaticImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_battery), + ) + .setAmbientImage( + ambientImage = Icon.createWithResource( + this, + R.drawable.ic_battery_burn_protect, + ), + ) + .build() + title = null + caseContentDescription = getString( + R.string.goal_progress_text_with_icon_content_description, + ) + } + Case.TEXT_WITH_TITLE -> { + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_title), + ).build() + monochromaticImage = null + title = PlainComplicationText.Builder( + text = getText(R.string.short_title), + ).build() + + caseContentDescription = getString( + R.string.goal_progress_text_with_title_content_description, + ) + } + Case.ICON_ONLY -> { + text = null + monochromaticImage = MonochromaticImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_event_vd_theme_24), + ).build() + title = null + caseContentDescription = getString( + R.string.goal_progress_icon_only_content_description, + ) + } + } + + // Create a content description that includes the value information + val contentDescription = PlainComplicationText.Builder( + text = getString( + R.string.goal_progress_content_description, + caseContentDescription, + currentValue, + percentage, + case.targetValue, + ), + ) + .build() + + return GoalProgressComplicationData.Builder( + value = currentValue, + targetValue = case.targetValue, + contentDescription = contentDescription, + ) + .setText(text) + .setMonochromaticImage(monochromaticImage) + .setTitle(title) + .setTapAction(tapAction) + .build() + } + + private enum class Case( + val targetValue: Float, + ) { + TEXT_ONLY(5000f), + TEXT_WITH_ICON(2500f), + TEXT_WITH_TITLE(10000f), + ICON_ONLY(10000f), + } +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/IconDataSourceService.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/IconDataSourceService.kt new file mode 100644 index 000000000..91ff7680c --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/IconDataSourceService.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.app.PendingIntent +import android.content.ComponentName +import android.graphics.drawable.Icon +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.MonochromaticImage +import androidx.wear.watchface.complications.data.MonochromaticImageComplicationData +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService + +/** + * A complication provider that supports only [ComplicationType.MONOCHROMATIC_IMAGE] and cycles through + * a few different icons on each tap. + */ +class IconDataSourceService : SuspendingComplicationDataSourceService() { + + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + if (request.complicationType != ComplicationType.MONOCHROMATIC_IMAGE) { + return NoDataComplicationData() + } + val args = ComplicationToggleArgs( + providerComponent = ComponentName(this, javaClass), + complication = Complication.ICON, + complicationInstanceId = request.complicationInstanceId, + ) + val complicationTogglePendingIntent = + ComplicationToggleReceiver.getComplicationToggleIntent( + context = this, + args = args, + ) + // Suspending function to retrieve the complication's state + val state = args.getState(this@IconDataSourceService) + val case = Case.entries[state.mod(Case.entries.size)] + return getComplicationData( + tapAction = complicationTogglePendingIntent, + case = case, + ) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData = + getComplicationData( + tapAction = null, + case = Case.FACE, + ) + + private fun getComplicationData( + tapAction: PendingIntent?, + case: Case, + ): ComplicationData = + when (case) { + Case.FACE -> MonochromaticImageComplicationData.Builder( + monochromaticImage = MonochromaticImage.Builder( + Icon.createWithResource(this, R.drawable.ic_face_vd_theme_24), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.icon_face_content_description), + ).build(), + ) + Case.BATTERY -> MonochromaticImageComplicationData.Builder( + monochromaticImage = MonochromaticImage.Builder( + Icon.createWithResource(this, R.drawable.ic_battery), + ) + .setAmbientImage( + Icon.createWithResource(this, R.drawable.ic_battery_burn_protect), + ) + .build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.icon_battery_content_description), + ).build(), + ) + Case.EVENT -> MonochromaticImageComplicationData.Builder( + monochromaticImage = MonochromaticImage.Builder( + Icon.createWithResource(this, R.drawable.ic_event_vd_theme_24), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.icon_event_content_description), + ).build(), + ) + } + .setTapAction(tapAction) + .build() + + private enum class Case { + FACE, BATTERY, EVENT + } +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/LargeImageDataSourceService.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/LargeImageDataSourceService.kt new file mode 100644 index 000000000..5abf4a647 --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/LargeImageDataSourceService.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.app.PendingIntent +import android.content.ComponentName +import android.graphics.drawable.Icon +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.PhotoImageComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService + +/** + * A complication provider that supports only [ComplicationType.PHOTO_IMAGE] and cycles + * between a couple of images on tap. + */ +class LargeImageDataSourceService : SuspendingComplicationDataSourceService() { + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + if (request.complicationType != ComplicationType.PHOTO_IMAGE) { + return NoDataComplicationData() + } + val args = ComplicationToggleArgs( + providerComponent = ComponentName(this, javaClass), + complication = Complication.LARGE_IMAGE, + complicationInstanceId = request.complicationInstanceId, + ) + + // On many watch faces a large image complication might not respond to taps as the + // complication is used to provide the background for the watch. Providers should not rely + // on tap functionality for large image complications, but the tap action is still included + // here in case it is supported. + val complicationTogglePendingIntent = + ComplicationToggleReceiver.getComplicationToggleIntent( + context = this, + args = args, + ) + // Suspending function to retrieve the complication's state + val state = args.getState(this) + val case = Case.entries[state.mod(Case.entries.size)] + return getComplicationData( + tapAction = complicationTogglePendingIntent, + case = case, + ) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData? = + getComplicationData( + tapAction = null, + case = Case.AQUARIUM, + ) + + private fun getComplicationData( + tapAction: PendingIntent?, + case: Case, + ): ComplicationData = + when (case) { + Case.AQUARIUM -> PhotoImageComplicationData.Builder( + photoImage = Icon.createWithResource(this, R.drawable.aquarium), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.photo_image_aquarium_content_description), + ).build(), + ) + Case.OUTDOORS -> PhotoImageComplicationData.Builder( + photoImage = Icon.createWithResource(this, R.drawable.outdoors), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.photo_image_outdoors_content_description), + ).build(), + ) + } + .setTapAction(tapAction) + .build() + + private enum class Case { + AQUARIUM, OUTDOORS + } +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/LongTextDataSourceService.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/LongTextDataSourceService.kt new file mode 100644 index 000000000..105fed373 --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/LongTextDataSourceService.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.app.PendingIntent +import android.content.ComponentName +import android.graphics.drawable.Icon +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.LongTextComplicationData +import androidx.wear.watchface.complications.data.MonochromaticImage +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.SmallImage +import androidx.wear.watchface.complications.data.SmallImageType +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService + +/** + * A complication provider that supports only [ComplicationType.LONG_TEXT] and cycles + * through the possible configurations on tap. + */ +class LongTextDataSourceService : SuspendingComplicationDataSourceService() { + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + if (request.complicationType != ComplicationType.LONG_TEXT) { + return NoDataComplicationData() + } + val args = ComplicationToggleArgs( + providerComponent = ComponentName(this, javaClass), + complication = Complication.LONG_TEXT, + complicationInstanceId = request.complicationInstanceId, + ) + val complicationTogglePendingIntent = + ComplicationToggleReceiver.getComplicationToggleIntent( + context = this, + args = args, + ) + // Suspending function to retrieve the complication's state + val state = args.getState(this) + val case = Case.entries[state.mod(Case.entries.size)] + return getComplicationData( + tapAction = complicationTogglePendingIntent, + case = case, + ) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData = + getComplicationData( + tapAction = null, + case = Case.TEXT_WITH_ICON_AND_TITLE, + ) + + private fun getComplicationData( + tapAction: PendingIntent?, + case: Case, + ): ComplicationData = + when (case) { + Case.TEXT_ONLY -> LongTextComplicationData.Builder( + text = PlainComplicationText.Builder( + text = getText(R.string.long_text_only), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.long_text_only_content_description), + ).build(), + ) + Case.TEXT_WITH_ICON -> LongTextComplicationData.Builder( + text = PlainComplicationText.Builder( + text = getText(R.string.long_text_with_icon), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.long_text_with_icon_content_description), + ).build(), + ) + .setMonochromaticImage( + MonochromaticImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_face_vd_theme_24), + ).build(), + ) + // Unlike for short text complications, if the long title field is supplied then it + // should always be displayed by the watch face. This means that when a long text + // provider supplies both title and icon, it is expected that both are displayed. + Case.TEXT_WITH_ICON_AND_TITLE -> LongTextComplicationData.Builder( + text = PlainComplicationText.Builder( + text = getText(R.string.long_text_with_icon_and_title), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.long_text_with_icon_and_title_content_description), + ).build(), + ) + .setTitle( + PlainComplicationText.Builder( + text = getText(R.string.long_title), + ).build(), + ) + .setMonochromaticImage( + MonochromaticImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_battery), + ) + .setAmbientImage( + ambientImage = Icon.createWithResource( + this, + R.drawable.ic_battery_burn_protect, + ), + ) + .build(), + ) + Case.TEXT_WITH_TITLE -> LongTextComplicationData.Builder( + text = PlainComplicationText.Builder( + text = getText(R.string.long_text_with_title), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.long_text_with_title_content_description), + ).build(), + ) + .setTitle( + PlainComplicationText.Builder( + text = getText(R.string.long_title), + ).build(), + ) + Case.TEXT_WITH_IMAGE -> LongTextComplicationData.Builder( + text = PlainComplicationText.Builder( + text = getText(R.string.long_text_with_image), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.long_text_with_image_content_description), + ).build(), + ) + .setSmallImage( + SmallImage.Builder( + image = Icon.createWithResource(this, R.drawable.outdoors), + type = SmallImageType.PHOTO, + ).build(), + ) + Case.TEXT_WITH_IMAGE_AND_TITLE -> LongTextComplicationData.Builder( + text = PlainComplicationText.Builder( + text = getText(R.string.long_text_with_image_and_title), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.long_text_with_image_and_title_content_description), + ).build(), + ) + .setTitle( + PlainComplicationText.Builder( + text = getText(R.string.long_title), + ).build(), + ) + .setSmallImage( + SmallImage.Builder( + image = Icon.createWithResource(this, R.drawable.aquarium), + type = SmallImageType.PHOTO, + ).build(), + ) + } + .setTapAction(tapAction) + .build() + + private enum class Case { + TEXT_ONLY, + TEXT_WITH_ICON, + TEXT_WITH_ICON_AND_TITLE, + TEXT_WITH_TITLE, + TEXT_WITH_IMAGE, + TEXT_WITH_IMAGE_AND_TITLE, + } +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/NoDataDataSourceService.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/NoDataDataSourceService.kt new file mode 100644 index 000000000..dc579497b --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/NoDataDataSourceService.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService +import androidx.wear.watchface.complications.datasource.ComplicationRequest + +/** + * A complication provider that always returns [ComplicationType.NO_DATA]. + */ +class NoDataDataSourceService : ComplicationDataSourceService() { + override fun onComplicationRequest( + request: ComplicationRequest, + listener: ComplicationRequestListener, + ) { + listener.onComplicationData(NoDataComplicationData()) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData = + NoDataComplicationData() +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/RangedValueDataSourceService.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/RangedValueDataSourceService.kt new file mode 100644 index 000000000..34dc5f68a --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/RangedValueDataSourceService.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.app.PendingIntent +import android.content.ComponentName +import android.graphics.drawable.Icon +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationText +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.MonochromaticImage +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.RangedValueComplicationData +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService +import kotlin.random.Random + +/** + * A complication provider that supports only [ComplicationType.RANGED_VALUE] and cycles + * through the possible configurations on tap. The value is randomised on each update. + */ +class RangedValueDataSourceService : SuspendingComplicationDataSourceService() { + + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + if (request.complicationType != ComplicationType.RANGED_VALUE) { + return NoDataComplicationData() + } + val args = ComplicationToggleArgs( + providerComponent = ComponentName(this, javaClass), + complication = Complication.RANGED_VALUE, + complicationInstanceId = request.complicationInstanceId, + ) + val complicationTogglePendingIntent = + ComplicationToggleReceiver.getComplicationToggleIntent( + context = this, + args = args, + ) + // Suspending function to retrieve the complication's state + val state = args.getState(this) + val case = Case.entries[state.mod(Case.entries.size)] + + return getComplicationData( + tapAction = complicationTogglePendingIntent, + case = case, + ) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData? = + getComplicationData( + tapAction = null, + case = Case.TEXT_WITH_ICON, + ) + + private fun getComplicationData( + tapAction: PendingIntent?, + case: Case, + ): ComplicationData { + val text: ComplicationText? + val monochromaticImage: MonochromaticImage? + val title: ComplicationText? + val caseContentDescription: String + + val minValue = case.minValue + val maxValue = case.maxValue + val value = Random.nextDouble(minValue.toDouble(), maxValue.toDouble()).toFloat() + val percentage = (value - minValue) / (maxValue - minValue) + + when (case) { + Case.TEXT_ONLY -> { + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_only), + ).build() + monochromaticImage = null + title = null + caseContentDescription = getString( + R.string.ranged_value_text_only_content_description, + ) + } + Case.TEXT_WITH_ICON -> { + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_icon), + ).build() + monochromaticImage = MonochromaticImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_battery), + ) + .setAmbientImage( + ambientImage = Icon.createWithResource( + this, + R.drawable.ic_battery_burn_protect, + ), + ) + .build() + title = null + caseContentDescription = getString( + R.string.ranged_value_text_with_icon_content_description, + ) + } + Case.TEXT_WITH_TITLE -> { + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_title), + ).build() + monochromaticImage = null + title = PlainComplicationText.Builder( + text = getText(R.string.short_title), + ).build() + + caseContentDescription = getString( + R.string.ranged_value_text_with_title_content_description, + ) + } + Case.ICON_ONLY -> { + text = null + monochromaticImage = MonochromaticImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_event_vd_theme_24), + ).build() + title = null + caseContentDescription = getString( + R.string.ranged_value_icon_only_content_description, + ) + } + } + + // Create a content description that includes the value information + val contentDescription = PlainComplicationText.Builder( + text = getString( + R.string.ranged_value_content_description, + caseContentDescription, + value, + percentage, + minValue, + maxValue, + ), + ) + .build() + + return RangedValueComplicationData.Builder( + value = value, + min = minValue, + max = maxValue, + contentDescription = contentDescription, + ) + .setText(text) + .setMonochromaticImage(monochromaticImage) + .setTitle(title) + .setTapAction(tapAction) + .build() + } + + private enum class Case( + val minValue: Float, + val maxValue: Float, + ) { + TEXT_ONLY(0f, 100f), + TEXT_WITH_ICON(-20f, 20f), + TEXT_WITH_TITLE(57.5f, 824.2f), + ICON_ONLY(10_045f, 100_000f), + ; + + init { + require(minValue < maxValue) { "Minimum value was greater than maximum value!" } + } + } +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/ShortTextDataSourceService.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/ShortTextDataSourceService.kt new file mode 100644 index 000000000..f5be92a1e --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/ShortTextDataSourceService.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.app.PendingIntent +import android.content.ComponentName +import android.graphics.drawable.Icon +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.MonochromaticImage +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.ShortTextComplicationData +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService + +/** + * A complication provider that supports only [ComplicationType.SHORT_TEXT] and cycles + * through the possible configurations on tap. + */ +class ShortTextDataSourceService : SuspendingComplicationDataSourceService() { + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + if (request.complicationType != ComplicationType.SHORT_TEXT) { + return NoDataComplicationData() + } + val args = ComplicationToggleArgs( + providerComponent = ComponentName(this, javaClass), + complication = Complication.SHORT_TEXT, + complicationInstanceId = request.complicationInstanceId, + ) + val complicationTogglePendingIntent = + ComplicationToggleReceiver.getComplicationToggleIntent( + context = this, + args = args, + ) + // Suspending function to retrieve the complication's state + val state = args.getState(this) + val case = Case.entries[state.mod(Case.entries.size)] + return getComplicationData( + tapAction = complicationTogglePendingIntent, + case = case, + ) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData = + getComplicationData( + tapAction = null, + case = Case.TEXT_WITH_ICON_AND_TITLE, + ) + + private fun getComplicationData( + tapAction: PendingIntent?, + case: Case, + ): ComplicationData = + when (case) { + Case.TEXT_ONLY -> ShortTextComplicationData.Builder( + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_only), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.short_text_only_content_description), + ).build(), + ) + Case.TEXT_WITH_ICON -> ShortTextComplicationData.Builder( + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_icon), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_icon_content_description), + ).build(), + ) + .setMonochromaticImage( + MonochromaticImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_face_vd_theme_24), + ).build(), + ) + Case.TEXT_WITH_TITLE -> ShortTextComplicationData.Builder( + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_title), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_title_content_description), + ).build(), + ) + .setTitle( + PlainComplicationText.Builder( + text = getText(R.string.short_title), + ).build(), + ) + Case.TEXT_WITH_ICON_AND_TITLE -> ShortTextComplicationData.Builder( + // When short text includes both short title and icon, the watch face should only + // display one of those fields. + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_both), + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_both_content_description), + ).build(), + ) + .setTitle( + PlainComplicationText.Builder( + text = getText(R.string.short_title), + ).build(), + ) + .setMonochromaticImage( + MonochromaticImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_face_vd_theme_24), + ).build(), + ) + } + .setTapAction(tapAction) + .build() + + private enum class Case { + TEXT_ONLY, + TEXT_WITH_ICON, + TEXT_WITH_TITLE, + TEXT_WITH_ICON_AND_TITLE, + } +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/SmallImageDataSourceService.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/SmallImageDataSourceService.kt new file mode 100644 index 000000000..e9bf858ad --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/SmallImageDataSourceService.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.app.PendingIntent +import android.content.ComponentName +import android.graphics.drawable.Icon +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.SmallImage +import androidx.wear.watchface.complications.data.SmallImageComplicationData +import androidx.wear.watchface.complications.data.SmallImageType +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService + +/** + * A complication provider that supports only [ComplicationType.SMALL_IMAGE] and cycles + * between the different image styles on tap. + */ +class SmallImageDataSourceService : SuspendingComplicationDataSourceService() { + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + if (request.complicationType != ComplicationType.SMALL_IMAGE) { + return NoDataComplicationData() + } + val args = ComplicationToggleArgs( + providerComponent = ComponentName(this, javaClass), + complication = Complication.SMALL_IMAGE, + complicationInstanceId = request.complicationInstanceId, + ) + val complicationTogglePendingIntent = + ComplicationToggleReceiver.getComplicationToggleIntent( + context = this, + args = args, + ) + // Suspending function to retrieve the complication's state + val state = args.getState(this) + val case = Case.entries[state.mod(Case.entries.size)] + return getComplicationData( + tapAction = complicationTogglePendingIntent, + case = case, + ) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData = + getComplicationData( + tapAction = null, + case = Case.PHOTO, + ) + + private fun getComplicationData( + tapAction: PendingIntent?, + case: Case, + ): ComplicationData = + when (case) { + Case.PHOTO -> SmallImageComplicationData.Builder( + // An image using IMAGE_STYLE_PHOTO may be cropped to fill the space given to it. + smallImage = SmallImage.Builder( + image = Icon.createWithResource(this, R.drawable.aquarium), + type = SmallImageType.PHOTO, + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.small_image_photo_content_description), + ).build(), + ) + Case.ICON -> SmallImageComplicationData.Builder( + // An image using IMAGE_STYLE_ICON must not be cropped, and should fit within the + // space given to it. + smallImage = SmallImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_launcher), + type = SmallImageType.ICON, + ).build(), + contentDescription = PlainComplicationText.Builder( + text = getText(R.string.small_image_icon_content_description), + ).build(), + ) + } + .setTapAction(tapAction) + .build() + + private enum class Case { + PHOTO, ICON + } +} diff --git a/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/WeightedElementsDataSourceService.kt b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/WeightedElementsDataSourceService.kt new file mode 100644 index 000000000..9ab9856c3 --- /dev/null +++ b/Complications/Wearable/src/main/java/com/example/android/wearable/wear/complications/WeightedElementsDataSourceService.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.wearable.wear.complications + +import android.app.PendingIntent +import android.content.ComponentName +import android.graphics.Color +import android.graphics.drawable.Icon +import androidx.annotation.RequiresApi +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationText +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.MonochromaticImage +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.WeightedElementsComplicationData +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService +import kotlin.random.Random + +/** + * A complication provider that supports only [ComplicationType.WEIGHTED_ELEMENTS] and cycles + * through the possible configurations on tap. The value is randomised on each update. + */ +@RequiresApi(33) +class WeightedElementsDataSourceService : SuspendingComplicationDataSourceService() { + + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + if (request.complicationType != ComplicationType.WEIGHTED_ELEMENTS) { + return NoDataComplicationData() + } + val args = ComplicationToggleArgs( + providerComponent = ComponentName(this, javaClass), + complication = Complication.WEIGHTED_ELEMENTS, + complicationInstanceId = request.complicationInstanceId, + ) + val complicationTogglePendingIntent = + ComplicationToggleReceiver.getComplicationToggleIntent( + context = this, + args = args, + ) + // Suspending function to retrieve the complication's state + val state = args.getState(this) + val case = Case.entries[state.mod(Case.entries.size)] + + return getComplicationData( + tapAction = complicationTogglePendingIntent, + case = case, + ) + } + + override fun getPreviewData(type: ComplicationType): ComplicationData? = + getComplicationData( + tapAction = null, + case = Case.TEXT_WITH_ICON, + ) + + private fun getComplicationData( + tapAction: PendingIntent?, + case: Case, + ): ComplicationData { + val text: ComplicationText? + val monochromaticImage: MonochromaticImage? + val title: ComplicationText? + val caseContentDescription: String + + when (case) { + Case.TEXT_ONLY -> { + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_only), + ).build() + monochromaticImage = null + title = null + caseContentDescription = getString( + R.string.weighted_elements_text_only_content_description, + ) + } + Case.TEXT_WITH_ICON -> { + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_icon), + ).build() + monochromaticImage = MonochromaticImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_battery), + ) + .setAmbientImage( + ambientImage = Icon.createWithResource( + this, + R.drawable.ic_battery_burn_protect, + ), + ) + .build() + title = null + caseContentDescription = getString( + R.string.weighted_elements_text_with_icon_content_description, + ) + } + Case.TEXT_WITH_TITLE -> { + text = PlainComplicationText.Builder( + text = getText(R.string.short_text_with_title), + ).build() + monochromaticImage = null + title = PlainComplicationText.Builder( + text = getText(R.string.short_title), + ).build() + + caseContentDescription = getString( + R.string.weighted_elements_text_with_title_content_description, + ) + } + Case.ICON_ONLY -> { + text = null + monochromaticImage = MonochromaticImage.Builder( + image = Icon.createWithResource(this, R.drawable.ic_event_vd_theme_24), + ).build() + title = null + caseContentDescription = getString( + R.string.weighted_elements_icon_only_content_description, + ) + } + } + + // Create a content description that includes the value information + val contentDescription = PlainComplicationText.Builder( + text = getString( + R.string.weighted_elements_content_description, + caseContentDescription, + case.numElements, + resources.getQuantityString(R.plurals.number_of_elements, case.numElements), + ), + ) + .build() + + return WeightedElementsComplicationData.Builder( + elements = createWeightedElements(case.numElements), + contentDescription = contentDescription, + ) + .setText(text) + .setMonochromaticImage(monochromaticImage) + .setTitle(title) + .setTapAction(tapAction) + .build() + } + + private fun createWeightedElements(numElements: Int): + List { + val elements = mutableListOf() + repeat(numElements) { index -> + val weight = Random.nextInt(1, 3).toFloat() + val color = colors[(index % colors.size)] + elements.add(WeightedElementsComplicationData.Element(weight, color)) + } + return elements + } + + private enum class Case( + val numElements: Int, + ) { + TEXT_ONLY(5), + TEXT_WITH_ICON(3), + TEXT_WITH_TITLE(4), + ICON_ONLY(4), + } + + private val colors = listOf( + Color.argb(255, 255, 0, 0), + Color.argb(255, 0, 255, 0), + Color.argb(255, 0, 0, 255), + ) +} diff --git a/Complications/Wearable/src/main/res/drawable-hdpi/ic_launcher.png b/Complications/Wearable/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..589f229d1 Binary files /dev/null and b/Complications/Wearable/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/Complications/Wearable/src/main/res/drawable-mdpi/ic_launcher.png b/Complications/Wearable/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..77dd57139 Binary files /dev/null and b/Complications/Wearable/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/Complications/Wearable/src/main/res/drawable-nodpi/aquarium.png b/Complications/Wearable/src/main/res/drawable-nodpi/aquarium.png new file mode 100644 index 000000000..cde6953b8 Binary files /dev/null and b/Complications/Wearable/src/main/res/drawable-nodpi/aquarium.png differ diff --git a/Complications/Wearable/src/main/res/drawable-nodpi/outdoors.png b/Complications/Wearable/src/main/res/drawable-nodpi/outdoors.png new file mode 100644 index 000000000..2679aff64 Binary files /dev/null and b/Complications/Wearable/src/main/res/drawable-nodpi/outdoors.png differ diff --git a/Complications/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png b/Complications/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..fe34ebe13 Binary files /dev/null and b/Complications/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/Complications/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png b/Complications/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..ab80bcd13 Binary files /dev/null and b/Complications/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/Complications/Wearable/src/main/res/drawable/ic_battery.xml b/Complications/Wearable/src/main/res/drawable/ic_battery.xml new file mode 100644 index 000000000..27ac6683e --- /dev/null +++ b/Complications/Wearable/src/main/res/drawable/ic_battery.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/Complications/Wearable/src/main/res/drawable/ic_battery_burn_protect.xml b/Complications/Wearable/src/main/res/drawable/ic_battery_burn_protect.xml new file mode 100644 index 000000000..20f22aaf2 --- /dev/null +++ b/Complications/Wearable/src/main/res/drawable/ic_battery_burn_protect.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/Complications/Wearable/src/main/res/drawable/ic_data_usage_vd_theme_24.xml b/Complications/Wearable/src/main/res/drawable/ic_data_usage_vd_theme_24.xml new file mode 100644 index 000000000..503718368 --- /dev/null +++ b/Complications/Wearable/src/main/res/drawable/ic_data_usage_vd_theme_24.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/Complications/Wearable/src/main/res/drawable/ic_event_vd_theme_24.xml b/Complications/Wearable/src/main/res/drawable/ic_event_vd_theme_24.xml new file mode 100644 index 000000000..d8c54e9c7 --- /dev/null +++ b/Complications/Wearable/src/main/res/drawable/ic_event_vd_theme_24.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/Complications/Wearable/src/main/res/drawable/ic_face_vd_theme_24.xml b/Complications/Wearable/src/main/res/drawable/ic_face_vd_theme_24.xml new file mode 100644 index 000000000..5ba72fa6e --- /dev/null +++ b/Complications/Wearable/src/main/res/drawable/ic_face_vd_theme_24.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/Complications/Wearable/src/main/res/drawable/ic_image_vd_theme_24.xml b/Complications/Wearable/src/main/res/drawable/ic_image_vd_theme_24.xml new file mode 100644 index 000000000..cbbfc6b0b --- /dev/null +++ b/Complications/Wearable/src/main/res/drawable/ic_image_vd_theme_24.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/Complications/Wearable/src/main/res/drawable/ic_landscape_vd_theme_24.xml b/Complications/Wearable/src/main/res/drawable/ic_landscape_vd_theme_24.xml new file mode 100644 index 000000000..f88d47b68 --- /dev/null +++ b/Complications/Wearable/src/main/res/drawable/ic_landscape_vd_theme_24.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/Complications/Wearable/src/main/res/drawable/ic_not_interested_vd_theme_24.xml b/Complications/Wearable/src/main/res/drawable/ic_not_interested_vd_theme_24.xml new file mode 100644 index 000000000..332dea03c --- /dev/null +++ b/Complications/Wearable/src/main/res/drawable/ic_not_interested_vd_theme_24.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/Complications/Wearable/src/main/res/drawable/ic_short_text_vd_theme_24.xml b/Complications/Wearable/src/main/res/drawable/ic_short_text_vd_theme_24.xml new file mode 100644 index 000000000..76c4090dc --- /dev/null +++ b/Complications/Wearable/src/main/res/drawable/ic_short_text_vd_theme_24.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/Complications/Wearable/src/main/res/drawable/ic_title_vd_theme_24.xml b/Complications/Wearable/src/main/res/drawable/ic_title_vd_theme_24.xml new file mode 100644 index 000000000..aba1cd13b --- /dev/null +++ b/Complications/Wearable/src/main/res/drawable/ic_title_vd_theme_24.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/Complications/Wearable/src/main/res/values/strings.xml b/Complications/Wearable/src/main/res/values/strings.xml new file mode 100644 index 000000000..3e561438f --- /dev/null +++ b/Complications/Wearable/src/main/res/values/strings.xml @@ -0,0 +1,86 @@ + + + + Complications + + No Data + Short Text + Ranged Value + Goal Progress + Long Text + Icon + Small Image + Large Image + Weighted Elements + + Face + Battery + Event + + Photo + Icon + + Aquarium + Outdoors + + Text only + Text with icon + Text with title + Icon only + %1$s at %2$.2f, %3$.0f%% between %4$.2f and %5$.2f + + Text only + Text with icon + Text with title + Icon only + %1$s at %2$.2f, %3$.0f%%. Target %4$.2f + + Text only + Text with icon + Text with title + Icon only + %1$s with %2$d %3$s + + TxtOnly + Short Text Only + w/Icon + Short Text with Icon + w/Title + Short Text with Icon + w/Both + Short Text with Icon and Title + Title + + Text Only + @string/long_text_only + Text w/ Title + Text with Title + Text w/ Icon + Text with Icon + Text w/ Icon & Title + Text with Icon and Title + Text with Image + @string/long_text_with_image + Text w/ Image & Title + Text with Image and Title + Long Title + + + element + elements + + \ No newline at end of file diff --git a/Complications/Wearable/src/main/res/values/styles.xml b/Complications/Wearable/src/main/res/values/styles.xml new file mode 100644 index 000000000..eda0b2575 --- /dev/null +++ b/Complications/Wearable/src/main/res/values/styles.xml @@ -0,0 +1,20 @@ + + + +