Skip to content

Commit

Permalink
Merge pull request #1077 from android/complications-suite
Browse files Browse the repository at this point in the history
Adds support for GOAL_PROGRESS and WEIGHTED_ELEMENTS complications.
  • Loading branch information
garanj authored Apr 24, 2024
2 parents dbc51b5 + dbfff68 commit 13f3933
Show file tree
Hide file tree
Showing 13 changed files with 436 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,42 @@
android:value="0" />
</service>

<service
android:name=".GoalProgressDataSourceService"
android:exported="true"
android:icon="@drawable/ic_data_usage_vd_theme_24"
android:label="@string/goal_progress_label"
android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
<intent-filter>
<action android:name="android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST" />
</intent-filter>

<meta-data
android:name="android.support.wearable.complications.SUPPORTED_TYPES"
android:value="GOAL_PROGRESS" />
<meta-data
android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
android:value="0" />
</service>

<service
android:name=".WeightedElementsDataSourceService"
android:exported="true"
android:icon="@drawable/ic_data_usage_vd_theme_24"
android:label="@string/weighted_elements"
android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
<intent-filter>
<action android:name="android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST" />
</intent-filter>

<meta-data
android:name="android.support.wearable.complications.SUPPORTED_TYPES"
android:value="WEIGHTED_ELEMENTS" />
<meta-data
android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
android:value="0" />
</service>

<receiver android:name=".ComplicationToggleReceiver" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ enum class Complication(
*/
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"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ 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
Expand Down Expand Up @@ -68,7 +69,6 @@ class ComplicationToggleReceiver : BroadcastReceiver() {
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(
Expand All @@ -83,7 +83,11 @@ class ComplicationToggleReceiver : BroadcastReceiver() {
* Returns the [ComplicationToggleArgs] from the [Intent] sent to the [ComplicationToggleArgs].
*/
private fun Intent.getArgs(): ComplicationToggleArgs = requireNotNull(
extras?.getParcelable(EXTRA_ARGS),
BundleCompat.getParcelable(
this.extras!!,
EXTRA_ARGS,
ComplicationToggleArgs::class.java,
),
)
}
}
Original file line number Diff line number Diff line change
@@ -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.wearcomplicationproviderstestsuite

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),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,24 @@ package com.example.android.wearable.wear.wearcomplicationproviderstestsuite
import android.app.PendingIntent
import android.content.ComponentName
import android.graphics.drawable.Icon
import androidx.datastore.core.DataStore
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.ComplicationDataSourceService
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.
*
* Note: This subclasses [SuspendingComplicationDataSourceService] instead of [ComplicationDataSourceService] to support
* coroutines, so data operations (specifically, calls to [DataStore]) can be supported directly in the
* [onComplicationRequest].
*
* If you don't perform any suspending operations to update your complications, you can subclass
* [ComplicationDataSourceService] and override [onComplicationRequest] directly.
* (see [NoDataDataSourceService] for an example)
*/
class IconDataSourceService : SuspendingComplicationDataSourceService() {

override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? {
if (request.complicationType != ComplicationType.MONOCHROMATIC_IMAGE) {
return null
return NoDataComplicationData()
}
val args = ComplicationToggleArgs(
providerComponent = ComponentName(this, javaClass),
Expand All @@ -58,7 +49,7 @@ class IconDataSourceService : SuspendingComplicationDataSourceService() {
)
// Suspending function to retrieve the complication's state
val state = args.getState(this@IconDataSourceService)
val case = Case.values()[state.mod(Case.values().size)]
val case = Case.entries[state.mod(Case.entries.size)]
return getComplicationData(
tapAction = complicationTogglePendingIntent,
case = case,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,22 @@ package com.example.android.wearable.wear.wearcomplicationproviderstestsuite
import android.app.PendingIntent
import android.content.ComponentName
import android.graphics.drawable.Icon
import androidx.datastore.core.DataStore
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.ComplicationDataSourceService
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.
*
* Note: This subclasses [SuspendingComplicationDataSourceService] instead of [ComplicationDataSourceService] to support
* coroutines, so data operations (specifically, calls to [DataStore]) can be supported directly in the
* [onComplicationRequest].
*
* If you don't perform any suspending operations to update your complications, you can subclass
* [ComplicationDataSourceService] and override [onComplicationRequest] directly.
* (see [NoDataDataSourceService] for an example)
*/
class LargeImageDataSourceService : SuspendingComplicationDataSourceService() {
override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? {
if (request.complicationType != ComplicationType.PHOTO_IMAGE) {
return null
return NoDataComplicationData()
}
val args = ComplicationToggleArgs(
providerComponent = ComponentName(this, javaClass),
Expand All @@ -61,7 +52,7 @@ class LargeImageDataSourceService : SuspendingComplicationDataSourceService() {
)
// Suspending function to retrieve the complication's state
val state = args.getState(this)
val case = Case.values()[state.mod(Case.values().size)]
val case = Case.entries[state.mod(Case.entries.size)]
return getComplicationData(
tapAction = complicationTogglePendingIntent,
case = case,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,25 @@ package com.example.android.wearable.wear.wearcomplicationproviderstestsuite
import android.app.PendingIntent
import android.content.ComponentName
import android.graphics.drawable.Icon
import androidx.datastore.core.DataStore
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.ComplicationDataSourceService
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.
*
* Note: This subclasses [SuspendingComplicationDataSourceService] instead of [ComplicationDataSourceService] to support
* coroutines, so data operations (specifically, calls to [DataStore]) can be supported directly in the
* [onComplicationRequest].
*
* If you don't perform any suspending operations to update your complications, you can subclass
* [ComplicationDataSourceService] and override [onComplicationRequest] directly.
* (see [NoDataDataSourceService] for an example)
*/
class LongTextDataSourceService : SuspendingComplicationDataSourceService() {
override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? {
if (request.complicationType != ComplicationType.LONG_TEXT) {
return null
return NoDataComplicationData()
}
val args = ComplicationToggleArgs(
providerComponent = ComponentName(this, javaClass),
Expand All @@ -59,7 +50,7 @@ class LongTextDataSourceService : SuspendingComplicationDataSourceService() {
)
// Suspending function to retrieve the complication's state
val state = args.getState(this)
val case = Case.values()[state.mod(Case.values().size)]
val case = Case.entries[state.mod(Case.entries.size)]
return getComplicationData(
tapAction = complicationTogglePendingIntent,
case = case,
Expand Down
Loading

0 comments on commit 13f3933

Please sign in to comment.