diff --git a/api/auth/data/com.google.android.horologist.auth.data.oauth.pkce/-p-k-c-e-o-auth-code-repository/fetch.html b/api/auth/data/com.google.android.horologist.auth.data.oauth.pkce/-p-k-c-e-o-auth-code-repository/fetch.html index 4cafdbbff5..9cf5fdf3de 100644 --- a/api/auth/data/com.google.android.horologist.auth.data.oauth.pkce/-p-k-c-e-o-auth-code-repository/fetch.html +++ b/api/auth/data/com.google.android.horologist.auth.data.oauth.pkce/-p-k-c-e-o-auth-code-repository/fetch.html @@ -63,7 +63,7 @@

fetch

-
abstract suspend fun fetch(config: PKCEConfig, codeVerifier: CodeVerifier): Result<OAuthCodePayload>
+
abstract suspend fun fetch(config: PKCEConfig, codeVerifier: CodeVerifier): Result<OAuthCodePayload>
-
abstract suspend fun fetch(config: PKCEConfig, codeVerifier: CodeVerifier): Result<OAuthCodePayload>
+
abstract suspend fun fetch(config: PKCEConfig, codeVerifier: CodeVerifier): Result<OAuthCodePayload>
diff --git a/api/auth/data/com.google.android.horologist.auth.data.tokenshare.impl/-token-bundle-repository-impl/-companion/create.html b/api/auth/data/com.google.android.horologist.auth.data.tokenshare.impl/-token-bundle-repository-impl/-companion/create.html index 4466c1eb38..df17d40c32 100644 --- a/api/auth/data/com.google.android.horologist.auth.data.tokenshare.impl/-token-bundle-repository-impl/-companion/create.html +++ b/api/auth/data/com.google.android.horologist.auth.data.tokenshare.impl/-token-bundle-repository-impl/-companion/create.html @@ -63,7 +63,7 @@

create

-
fun <T> create(registry: WearDataLayerRegistry, serializer: Serializer<T>, key: String = DEFAULT_TOKEN_BUNDLE_KEY): TokenBundleRepositoryImpl<T>

Factory method for TokenBundleRepositoryImpl.

If multiple token bundles are available, specify the key of the specific token bundle wished to be retrieved. Otherwise the token bundle stored with the default key will be used.

+
fun <T> create(registry: WearDataLayerRegistry, serializer: Serializer<T>, key: String = DEFAULT_TOKEN_BUNDLE_KEY): TokenBundleRepositoryImpl<T>

Factory method for TokenBundleRepositoryImpl.

If multiple token bundles are available, specify the key of the specific token bundle wished to be retrieved. Otherwise the token bundle stored with the default key will be used.

-
fun <T> create(registry: WearDataLayerRegistry, serializer: Serializer<T>, key: String = DEFAULT_TOKEN_BUNDLE_KEY): TokenBundleRepositoryImpl<T>

Factory method for TokenBundleRepositoryImpl.

+
fun <T> create(registry: WearDataLayerRegistry, serializer: Serializer<T>, key: String = DEFAULT_TOKEN_BUNDLE_KEY): TokenBundleRepositoryImpl<T>

Factory method for TokenBundleRepositoryImpl.

diff --git a/api/auth/data/com.google.android.horologist.auth.data.tokenshare.impl/-token-bundle-repository-impl/-token-bundle-repository-impl.html b/api/auth/data/com.google.android.horologist.auth.data.tokenshare.impl/-token-bundle-repository-impl/-token-bundle-repository-impl.html index c07e6a30fc..bd5ad41b9a 100644 --- a/api/auth/data/com.google.android.horologist.auth.data.tokenshare.impl/-token-bundle-repository-impl/-token-bundle-repository-impl.html +++ b/api/auth/data/com.google.android.horologist.auth.data.tokenshare.impl/-token-bundle-repository-impl/-token-bundle-repository-impl.html @@ -63,7 +63,7 @@

TokenBundleRepositoryImpl

-
constructor(registry: WearDataLayerRegistry, serializer: Serializer<T>, path: String)
+
constructor(registry: WearDataLayerRegistry, serializer: Serializer<T>, path: String)
-
data class Ambient(val ambientDetails: AmbientLifecycleObserver.AmbientDetails? = null) : AmbientState
+
data class Ambient(val ambientDetails: AmbientLifecycleObserver.AmbientDetails? = null) : AmbientState
diff --git a/api/compose-layout/com.google.android.horologist.compose.ambient/index.html b/api/compose-layout/com.google.android.horologist.compose.ambient/index.html index e97d87fd04..58cb675434 100644 --- a/api/compose-layout/com.google.android.horologist.compose.ambient/index.html +++ b/api/compose-layout/com.google.android.horologist.compose.ambient/index.html @@ -127,7 +127,7 @@

Functions

-
@RequiresApi(value = 26)
fun AmbientAwareTime(stateUpdate: AmbientStateUpdate, updatePeriodMillis: Long = 1000, block: @Composable (dateTime: ZonedDateTime, isAmbient: Boolean) -> Unit)

An example of using AmbientAware: Provides the time, at the specified update frequency, whilst in interactive mode, or when ambient-generated updates occur (typically every 1 min).

+
@RequiresApi(value = 26)
fun AmbientAwareTime(stateUpdate: AmbientStateUpdate, updatePeriodMillis: Long = 1000, block: @Composable (dateTime: ZonedDateTime, isAmbient: Boolean) -> Unit)

An example of using AmbientAware: Provides the time, at the specified update frequency, whilst in interactive mode, or when ambient-generated updates occur (typically every 1 min).

diff --git a/api/compose-layout/com.google.android.horologist.compose.navscaffold/-nav-scaffold-view-model/-nav-scaffold-view-model.html b/api/compose-layout/com.google.android.horologist.compose.navscaffold/-nav-scaffold-view-model/-nav-scaffold-view-model.html index 3f76bc3e18..c1ca2b4c72 100644 --- a/api/compose-layout/com.google.android.horologist.compose.navscaffold/-nav-scaffold-view-model/-nav-scaffold-view-model.html +++ b/api/compose-layout/com.google.android.horologist.compose.navscaffold/-nav-scaffold-view-model/-nav-scaffold-view-model.html @@ -63,7 +63,7 @@

NavScaffoldViewModel

-
constructor(savedStateHandle: SavedStateHandle)
+
constructor(savedStateHandle: SavedStateHandle)
@@ -211,7 +211,7 @@

Functions

- +
diff --git a/api/compose-layout/com.google.android.horologist.compose.navscaffold/-non-scrollable-scaffold-context/-non-scrollable-scaffold-context.html b/api/compose-layout/com.google.android.horologist.compose.navscaffold/-non-scrollable-scaffold-context/-non-scrollable-scaffold-context.html index e178e164c4..2c2f783e40 100644 --- a/api/compose-layout/com.google.android.horologist.compose.navscaffold/-non-scrollable-scaffold-context/-non-scrollable-scaffold-context.html +++ b/api/compose-layout/com.google.android.horologist.compose.navscaffold/-non-scrollable-scaffold-context/-non-scrollable-scaffold-context.html @@ -63,7 +63,7 @@

NonScrollableScaffoldContext

-
constructor(backStackEntry: NavBackStackEntry, viewModel: NavScaffoldViewModel)
+
constructor(backStackEntry: NavBackStackEntry, viewModel: NavScaffoldViewModel)
@@ -113,7 +113,7 @@

Properties

diff --git a/api/compose-layout/com.google.android.horologist.compose.navscaffold/-scaffold-context/-scaffold-context.html b/api/compose-layout/com.google.android.horologist.compose.navscaffold/-scaffold-context/-scaffold-context.html index f64f4671e8..1d73235cdb 100644 --- a/api/compose-layout/com.google.android.horologist.compose.navscaffold/-scaffold-context/-scaffold-context.html +++ b/api/compose-layout/com.google.android.horologist.compose.navscaffold/-scaffold-context/-scaffold-context.html @@ -63,7 +63,7 @@

ScaffoldContext

-
constructor(backStackEntry: NavBackStackEntry, scrollableState: T, viewModel: NavScaffoldViewModel)
+
constructor(backStackEntry: NavBackStackEntry, scrollableState: T, viewModel: NavScaffoldViewModel)
@@ -113,7 +113,7 @@

Properties

diff --git a/api/compose-layout/com.google.android.horologist.compose.navscaffold/-scrollable-scaffold-context/-scrollable-scaffold-context.html b/api/compose-layout/com.google.android.horologist.compose.navscaffold/-scrollable-scaffold-context/-scrollable-scaffold-context.html index d4a4c4f114..cdc008f0d0 100644 --- a/api/compose-layout/com.google.android.horologist.compose.navscaffold/-scrollable-scaffold-context/-scrollable-scaffold-context.html +++ b/api/compose-layout/com.google.android.horologist.compose.navscaffold/-scrollable-scaffold-context/-scrollable-scaffold-context.html @@ -63,7 +63,7 @@

ScrollableScaffoldContext

-
constructor(backStackEntry: NavBackStackEntry, columnState: ScalingLazyColumnState, viewModel: NavScaffoldViewModel)
+
constructor(backStackEntry: NavBackStackEntry, columnState: ScalingLazyColumnState, viewModel: NavScaffoldViewModel)
@@ -113,7 +113,7 @@

Properties

diff --git a/api/compose-layout/com.google.android.horologist.compose.navscaffold/-wear-nav-scaffold.html b/api/compose-layout/com.google.android.horologist.compose.navscaffold/-wear-nav-scaffold.html index 549012a312..e5640d7977 100644 --- a/api/compose-layout/com.google.android.horologist.compose.navscaffold/-wear-nav-scaffold.html +++ b/api/compose-layout/com.google.android.horologist.compose.navscaffold/-wear-nav-scaffold.html @@ -63,11 +63,11 @@

WearNavScaffold

-
fun WearNavScaffold(startDestination: String, navController: NavHostController, modifier: Modifier = Modifier, snackbar: @Composable () -> Unit = {}, timeText: @Composable (Modifier) -> Unit = { +
fun WearNavScaffold(startDestination: String, navController: NavHostController, modifier: Modifier = Modifier, snackbar: @Composable () -> Unit = {}, timeText: @Composable (Modifier) -> Unit = { TimeText( modifier = it, ) - }, state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(), builder: NavGraphBuilder.() -> Unit)

A Navigation and Scroll aware Scaffold.

In addition to NavGraphBuilder.scrollable, 3 additional extensions are supported scalingLazyColumnComposable, scrollStateComposable and lazyListComposable.

These should be used to build the ScrollableState or FocusRequester as well as configure the behaviour of TimeText, PositionIndicator or Vignette.

+ },
state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(), builder: NavGraphBuilder.() -> Unit
)

A Navigation and Scroll aware Scaffold.

In addition to NavGraphBuilder.scrollable, 3 additional extensions are supported scalingLazyColumnComposable, scrollStateComposable and lazyListComposable.

These should be used to build the ScrollableState or FocusRequester as well as configure the behaviour of TimeText, PositionIndicator or Vignette.

-
open class NavScaffoldViewModel(savedStateHandle: SavedStateHandle) : ViewModel

A ViewModel that backs the WearNavScaffold to allow each composable to interact and effect the Scaffold positionIndicator, vignette and timeText.

+
open class NavScaffoldViewModel(savedStateHandle: SavedStateHandle) : ViewModel

A ViewModel that backs the WearNavScaffold to allow each composable to interact and effect the Scaffold positionIndicator, vignette and timeText.

@@ -93,7 +93,7 @@

Types

-
data class NonScrollableScaffoldContext(val backStackEntry: NavBackStackEntry, val viewModel: NavScaffoldViewModel)
+
data class NonScrollableScaffoldContext(val backStackEntry: NavBackStackEntry, val viewModel: NavScaffoldViewModel)
@@ -108,7 +108,7 @@

Types

-
data class ScaffoldContext<T : ScrollableState>(val backStackEntry: NavBackStackEntry, val scrollableState: T, val viewModel: NavScaffoldViewModel)

The context items provided to a navigation composable.

+
data class ScaffoldContext<T : ScrollableState>(val backStackEntry: NavBackStackEntry, val scrollableState: T, val viewModel: NavScaffoldViewModel)

The context items provided to a navigation composable.

@@ -123,7 +123,7 @@

Types

-
data class ScrollableScaffoldContext(val backStackEntry: NavBackStackEntry, val columnState: ScalingLazyColumnState, val viewModel: NavScaffoldViewModel)

The context items provided to a navigation composable.

+
data class ScrollableScaffoldContext(val backStackEntry: NavBackStackEntry, val columnState: ScalingLazyColumnState, val viewModel: NavScaffoldViewModel)

The context items provided to a navigation composable.

@@ -142,7 +142,7 @@

Functions

-
fun NavGraphBuilder.composable(route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), content: @Composable (NonScrollableScaffoldContext) -> Unit)

Add non scrolling screen to the navigation graph. The NavBackStackEntry and NavScaffoldViewModel are passed into the content block so that the Scaffold may be customised, such as disabling TimeText.

+
fun NavGraphBuilder.composable(route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), content: @Composable (NonScrollableScaffoldContext) -> Unit)

Add non scrolling screen to the navigation graph. The NavBackStackEntry and NavScaffoldViewModel are passed into the content block so that the Scaffold may be customised, such as disabling TimeText.

@@ -157,7 +157,7 @@

Functions

-
fun NavGraphBuilder.lazyListComposable(route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), lazyListStateBuilder: () -> LazyListState = { LazyListState() }, content: @Composable (ScaffoldContext<LazyListState>) -> Unit)

Add a screen to the navigation graph featuring a Lazy list such as LazyColumn.

+
fun NavGraphBuilder.lazyListComposable(route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), lazyListStateBuilder: () -> LazyListState = { LazyListState() }, content: @Composable (ScaffoldContext<LazyListState>) -> Unit)

Add a screen to the navigation graph featuring a Lazy list such as LazyColumn.

@@ -172,7 +172,7 @@

Functions

-
fun NavGraphBuilder.scrollable(route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), columnStateFactory: ScalingLazyColumnState.Factory = ScalingLazyColumnDefaults.responsive(), content: @Composable (ScrollableScaffoldContext) -> Unit)

Add a screen to the navigation graph featuring a ScalingLazyColumn.

+
fun NavGraphBuilder.scrollable(route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), columnStateFactory: ScalingLazyColumnState.Factory = ScalingLazyColumnDefaults.responsive(), content: @Composable (ScrollableScaffoldContext) -> Unit)

Add a screen to the navigation graph featuring a ScalingLazyColumn.

@@ -187,7 +187,7 @@

Functions

-
fun NavGraphBuilder.scrollStateComposable(route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), scrollStateBuilder: () -> ScrollState = { ScrollState(0) }, content: @Composable (ScaffoldContext<ScrollState>) -> Unit)

Add a screen to the navigation graph featuring a Scrollable item.

+
fun NavGraphBuilder.scrollStateComposable(route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), scrollStateBuilder: () -> ScrollState = { ScrollState(0) }, content: @Composable (ScaffoldContext<ScrollState>) -> Unit)

Add a screen to the navigation graph featuring a Scrollable item.

@@ -202,11 +202,11 @@

Functions

-
fun WearNavScaffold(startDestination: String, navController: NavHostController, modifier: Modifier = Modifier, snackbar: @Composable () -> Unit = {}, timeText: @Composable (Modifier) -> Unit = { +
fun WearNavScaffold(startDestination: String, navController: NavHostController, modifier: Modifier = Modifier, snackbar: @Composable () -> Unit = {}, timeText: @Composable (Modifier) -> Unit = { TimeText( modifier = it, ) - }, state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(), builder: NavGraphBuilder.() -> Unit)

A Navigation and Scroll aware Scaffold.

+ },
state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(), builder: NavGraphBuilder.() -> Unit
)

A Navigation and Scroll aware Scaffold.

diff --git a/api/compose-layout/com.google.android.horologist.compose.navscaffold/lazy-list-composable.html b/api/compose-layout/com.google.android.horologist.compose.navscaffold/lazy-list-composable.html index a1adc413ef..d35f04eacb 100644 --- a/api/compose-layout/com.google.android.horologist.compose.navscaffold/lazy-list-composable.html +++ b/api/compose-layout/com.google.android.horologist.compose.navscaffold/lazy-list-composable.html @@ -63,7 +63,7 @@

lazyListComposable

-
fun NavGraphBuilder.lazyListComposable(route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), lazyListStateBuilder: () -> LazyListState = { LazyListState() }, content: @Composable (ScaffoldContext<LazyListState>) -> Unit)

Add a screen to the navigation graph featuring a Lazy list such as LazyColumn.

The scrollState must be taken from the ScaffoldContext.

+
fun NavGraphBuilder.lazyListComposable(route: String, arguments: List<NamedNavArgument> = emptyList(), deepLinks: List<NavDeepLink> = emptyList(), lazyListStateBuilder: () -> LazyListState = { LazyListState() }, content: @Composable (ScaffoldContext<LazyListState>) -> Unit)

Add a screen to the navigation graph featuring a Lazy list such as LazyColumn.

The scrollState must be taken from the ScaffoldContext.

-
fun <T : Any> ScalingLazyListScope.items(items: LazyPagingItems<T>, key: (item: T) -> Any? = null, itemContent: @Composable ScalingLazyListItemScope.(value: T?) -> Unit)

Adds the LazyPagingItems and their content to the scope. The range from 0 (inclusive) to LazyPagingItems.itemCount (exclusive) always represents the full range of presentable items, because every event from PagingDataDiffer will trigger a recomposition.

+
fun <T : Any> ScalingLazyListScope.items(items: LazyPagingItems<T>, key: (item: T) -> Any? = null, itemContent: @Composable ScalingLazyListItemScope.(value: T?) -> Unit)

Adds the LazyPagingItems and their content to the scope. The range from 0 (inclusive) to LazyPagingItems.itemCount (exclusive) always represents the full range of presentable items, because every event from PagingDataDiffer will trigger a recomposition.

diff --git a/api/compose-layout/com.google.android.horologist.compose.paging/items.html b/api/compose-layout/com.google.android.horologist.compose.paging/items.html index 24d17c0bbe..059cbb9814 100644 --- a/api/compose-layout/com.google.android.horologist.compose.paging/items.html +++ b/api/compose-layout/com.google.android.horologist.compose.paging/items.html @@ -63,7 +63,7 @@

items

-
fun <T : Any> ScalingLazyListScope.items(items: LazyPagingItems<T>, key: (item: T) -> Any? = null, itemContent: @Composable ScalingLazyListItemScope.(value: T?) -> Unit)

Adds the LazyPagingItems and their content to the scope. The range from 0 (inclusive) to LazyPagingItems.itemCount (exclusive) always represents the full range of presentable items, because every event from PagingDataDiffer will trigger a recomposition.

Code from https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt

Parameters

items

the items received from a Flow of PagingData.

key

a factory of stable and unique keys representing the item. Using the same key for multiple items in the list is not allowed. Type of the key should be saveable via Bundle on Android. If null is passed the position in the list will represent the key. When you specify the key the scroll position will be maintained based on the key, which means if you add/remove items before the current visible item the item with the given key will be kept as the first visible one.

itemContent

the content displayed by a single item. In case the item is null, the itemContent method should handle the logic of displaying a placeholder instead of the main content displayed by an item which is not null.

Samples

androidx.paging.compose.samples.ItemsDemo
+
fun <T : Any> ScalingLazyListScope.items(items: LazyPagingItems<T>, key: (item: T) -> Any? = null, itemContent: @Composable ScalingLazyListItemScope.(value: T?) -> Unit)

Adds the LazyPagingItems and their content to the scope. The range from 0 (inclusive) to LazyPagingItems.itemCount (exclusive) always represents the full range of presentable items, because every event from PagingDataDiffer will trigger a recomposition.

Code from https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt

Parameters

items

the items received from a Flow of PagingData.

key

a factory of stable and unique keys representing the item. Using the same key for multiple items in the list is not allowed. Type of the key should be saveable via Bundle on Android. If null is passed the position in the list will represent the key. When you specify the key the scroll position will be maintained based on the key, which means if you add/remove items before the current visible item the item with the given key will be kept as the first visible one.

itemContent

the content displayed by a single item. In case the item is null, the itemContent method should handle the logic of displaying a placeholder instead of the main content displayed by an item which is not null.

Samples

androidx.paging.compose.samples.ItemsDemo
diff --git a/api/media/media3-logging/com.google.android.horologist.media3.logging/-analytics-event-logger/index.html b/api/media/media3-logging/com.google.android.horologist.media3.logging/-analytics-event-logger/index.html index 3bf9c0d9b2..0e5a61bd8a 100644 --- a/api/media/media3-logging/com.google.android.horologist.media3.logging/-analytics-event-logger/index.html +++ b/api/media/media3-logging/com.google.android.horologist.media3.logging/-analytics-event-logger/index.html @@ -62,7 +62,7 @@

AnalyticsEventLogger

-

Simple implementation of EventLogger for logging critical Media3 events.

Most logging behaviour is inherited from EventLogger.

+

Simple implementation of EventLogger for logging critical Media3 events.

Most logging behaviour is inherited from EventLogger.

@@ -98,7 +98,7 @@

Functions

@@ -113,7 +113,7 @@

Functions

-
open override fun onAudioCodecError(eventTime: AnalyticsListener.EventTime, audioCodecError: Exception)
+
open override fun onAudioCodecError(eventTime: AnalyticsListener.EventTime, audioCodecError: Exception)
@@ -128,7 +128,7 @@

Functions

@@ -143,7 +143,7 @@

Functions

@@ -158,7 +158,7 @@

Functions

@@ -173,7 +173,7 @@

Functions

@@ -188,7 +188,7 @@

Functions

-
open override fun onAudioInputFormatChanged(eventTime: AnalyticsListener.EventTime, format: Format, decoderReuseEvaluation: DecoderReuseEvaluation?)
+
open override fun onAudioInputFormatChanged(eventTime: AnalyticsListener.EventTime, format: Format, decoderReuseEvaluation: DecoderReuseEvaluation?)
@@ -203,7 +203,7 @@

Functions

@@ -218,7 +218,7 @@

Functions

@@ -233,7 +233,7 @@

Functions

-
open override fun onAudioSinkError(eventTime: AnalyticsListener.EventTime, audioSinkError: Exception)
+
open override fun onAudioSinkError(eventTime: AnalyticsListener.EventTime, audioSinkError: Exception)
@@ -248,7 +248,7 @@

Functions

@@ -263,7 +263,7 @@

Functions

@@ -278,7 +278,7 @@

Functions

-
open override fun onAudioUnderrun(eventTime: AnalyticsListener.EventTime, bufferSize: Int, bufferSizeMs: Long, elapsedSinceLastFeedMs: Long)
+
open override fun onAudioUnderrun(eventTime: AnalyticsListener.EventTime, bufferSize: Int, bufferSizeMs: Long, elapsedSinceLastFeedMs: Long)
@@ -293,7 +293,7 @@

Functions

@@ -308,7 +308,7 @@

Functions

-
open override fun onBandwidthEstimate(eventTime: AnalyticsListener.EventTime, totalLoadTimeMs: Int, totalBytesLoaded: Long, bitrateEstimate: Long)
+
open override fun onBandwidthEstimate(eventTime: AnalyticsListener.EventTime, totalLoadTimeMs: Int, totalBytesLoaded: Long, bitrateEstimate: Long)
@@ -323,7 +323,7 @@

Functions

@@ -338,7 +338,7 @@

Functions

@@ -353,7 +353,7 @@

Functions

@@ -368,7 +368,7 @@

Functions

-
open override fun onDownstreamFormatChanged(eventTime: AnalyticsListener.EventTime, mediaLoadData: MediaLoadData)
+
open override fun onDownstreamFormatChanged(eventTime: AnalyticsListener.EventTime, mediaLoadData: MediaLoadData)
@@ -383,7 +383,7 @@

Functions

@@ -398,7 +398,7 @@

Functions

@@ -413,7 +413,7 @@

Functions

@@ -428,7 +428,7 @@

Functions

@@ -443,7 +443,7 @@

Functions

@@ -458,7 +458,7 @@

Functions

@@ -473,7 +473,7 @@

Functions

@@ -488,7 +488,7 @@

Functions

@@ -503,7 +503,7 @@

Functions

-
open override fun onIsLoadingChanged(eventTime: AnalyticsListener.EventTime, isLoading: Boolean)
+
open override fun onIsLoadingChanged(eventTime: AnalyticsListener.EventTime, isLoading: Boolean)
@@ -518,7 +518,7 @@

Functions

@@ -533,7 +533,7 @@

Functions

@@ -548,7 +548,7 @@

Functions

-
open override fun onLoadCompleted(eventTime: AnalyticsListener.EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData)
+
open override fun onLoadCompleted(eventTime: AnalyticsListener.EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData)
@@ -563,7 +563,7 @@

Functions

-
open override fun onLoadError(eventTime: AnalyticsListener.EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData, error: IOException, wasCanceled: Boolean)
+
open override fun onLoadError(eventTime: AnalyticsListener.EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData, error: IOException, wasCanceled: Boolean)
@@ -578,7 +578,7 @@

Functions

-
open override fun onLoadStarted(eventTime: AnalyticsListener.EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData)
+
open override fun onLoadStarted(eventTime: AnalyticsListener.EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData)
@@ -593,7 +593,7 @@

Functions

@@ -608,7 +608,7 @@

Functions

@@ -623,7 +623,7 @@

Functions

-
open override fun onMediaMetadataChanged(eventTime: AnalyticsListener.EventTime, mediaMetadata: MediaMetadata)
+
open override fun onMediaMetadataChanged(eventTime: AnalyticsListener.EventTime, mediaMetadata: MediaMetadata)
@@ -638,7 +638,7 @@

Functions

@@ -653,7 +653,7 @@

Functions

@@ -668,7 +668,7 @@

Functions

-
open override fun onPlaybackStateChanged(eventTime: AnalyticsListener.EventTime, state: Int)
+
open override fun onPlaybackStateChanged(eventTime: AnalyticsListener.EventTime, state: Int)
@@ -683,7 +683,7 @@

Functions

@@ -698,7 +698,7 @@

Functions

-
open override fun onPlayerError(eventTime: AnalyticsListener.EventTime, error: PlaybackException)
+
open override fun onPlayerError(eventTime: AnalyticsListener.EventTime, error: PlaybackException)
@@ -713,7 +713,7 @@

Functions

@@ -728,7 +728,7 @@

Functions

@@ -743,7 +743,7 @@

Functions

@@ -758,7 +758,7 @@

Functions

-
open override fun onPlayWhenReadyChanged(eventTime: AnalyticsListener.EventTime, playWhenReady: Boolean, reason: Int)
+
open override fun onPlayWhenReadyChanged(eventTime: AnalyticsListener.EventTime, playWhenReady: Boolean, reason: Int)
@@ -773,7 +773,7 @@

Functions

@@ -788,7 +788,7 @@

Functions

@@ -803,7 +803,7 @@

Functions

@@ -818,7 +818,7 @@

Functions

@@ -833,7 +833,7 @@

Functions

@@ -848,7 +848,7 @@

Functions

@@ -863,7 +863,7 @@

Functions

@@ -878,7 +878,7 @@

Functions

@@ -893,7 +893,7 @@

Functions

@@ -908,7 +908,7 @@

Functions

@@ -923,7 +923,7 @@

Functions

@@ -938,7 +938,7 @@

Functions

@@ -953,7 +953,7 @@

Functions

@@ -968,7 +968,7 @@

Functions

@@ -983,7 +983,7 @@

Functions

@@ -998,7 +998,7 @@

Functions

@@ -1013,7 +1013,7 @@

Functions

@@ -1028,7 +1028,7 @@

Functions

@@ -1043,7 +1043,7 @@

Functions

@@ -1058,7 +1058,7 @@

Functions

@@ -1073,7 +1073,7 @@

Functions

diff --git a/api/media/media3-logging/com.google.android.horologist.media3.logging/-analytics-event-logger/on-audio-codec-error.html b/api/media/media3-logging/com.google.android.horologist.media3.logging/-analytics-event-logger/on-audio-codec-error.html index d5f446ca64..8c6256d316 100644 --- a/api/media/media3-logging/com.google.android.horologist.media3.logging/-analytics-event-logger/on-audio-codec-error.html +++ b/api/media/media3-logging/com.google.android.horologist.media3.logging/-analytics-event-logger/on-audio-codec-error.html @@ -63,7 +63,7 @@

onAudioCodecError

-
open override fun onAudioCodecError(eventTime: AnalyticsListener.EventTime, audioCodecError: Exception)
+
open override fun onAudioCodecError(eventTime: AnalyticsListener.EventTime, audioCodecError: Exception)
-
abstract fun showMessage(@StringRes message: Int)
+
abstract fun showMessage(@StringRes message: Int)
diff --git a/api/media/media3-logging/com.google.android.horologist.media3.logging/-error-reporter/show-message.html b/api/media/media3-logging/com.google.android.horologist.media3.logging/-error-reporter/show-message.html index 655ab7e1ab..be94e33abb 100644 --- a/api/media/media3-logging/com.google.android.horologist.media3.logging/-error-reporter/show-message.html +++ b/api/media/media3-logging/com.google.android.horologist.media3.logging/-error-reporter/show-message.html @@ -63,7 +63,7 @@

showMessage

-
abstract fun showMessage(@StringRes message: Int)
+
abstract fun showMessage(@StringRes message: Int)
@@ -143,7 +143,7 @@

Functions

-
open override fun onTransferEnd(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean)
+
open override fun onTransferEnd(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean)
@@ -158,7 +158,7 @@

Functions

-
open override fun onTransferInitializing(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean)
+
open override fun onTransferInitializing(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean)
@@ -173,7 +173,7 @@

Functions

-
open override fun onTransferStart(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean)
+
open override fun onTransferStart(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean)
diff --git a/api/media/media3-logging/com.google.android.horologist.media3.logging/-transfer-listener/on-bytes-transferred.html b/api/media/media3-logging/com.google.android.horologist.media3.logging/-transfer-listener/on-bytes-transferred.html index 4bf78caa15..88982f2b69 100644 --- a/api/media/media3-logging/com.google.android.horologist.media3.logging/-transfer-listener/on-bytes-transferred.html +++ b/api/media/media3-logging/com.google.android.horologist.media3.logging/-transfer-listener/on-bytes-transferred.html @@ -63,7 +63,7 @@

onBytesTransferred

-
open override fun onBytesTransferred(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean, bytesTransferred: Int)
+
open override fun onBytesTransferred(source: DataSource, dataSpec: DataSpec, isNetwork: Boolean, bytesTransferred: Int)
-

Simple implementation of EventLogger for logging critical Media3 events.

+

Simple implementation of EventLogger for logging critical Media3 events.

@@ -108,7 +108,7 @@

Types

-

Simple implementation of TransferListener and EventListener for networking activity.

+

Simple implementation of TransferListener and EventListener for networking activity.

diff --git a/api/media/media3-logging/com.google.android.horologist.media3.tracing/-tracing-listener/index.html b/api/media/media3-logging/com.google.android.horologist.media3.tracing/-tracing-listener/index.html index 6998b93719..c8368f006b 100644 --- a/api/media/media3-logging/com.google.android.horologist.media3.tracing/-tracing-listener/index.html +++ b/api/media/media3-logging/com.google.android.horologist.media3.tracing/-tracing-listener/index.html @@ -62,7 +62,7 @@

TracingListener

- +
@@ -98,7 +98,7 @@

Functions

@@ -113,7 +113,7 @@

Functions

@@ -128,7 +128,7 @@

Functions

@@ -143,7 +143,7 @@

Functions

-
open fun onCues(p0: CueGroup)
+
open fun onCues(p0: CueGroup)
@@ -158,7 +158,7 @@

Functions

@@ -188,7 +188,7 @@

Functions

-
open fun onEvents(p0: Player, p1: Player.Events)
+
open fun onEvents(p0: Player, p1: Player.Events)
@@ -248,7 +248,7 @@

Functions

- +
@@ -263,7 +263,7 @@

Functions

@@ -278,7 +278,7 @@

Functions

- +
@@ -293,7 +293,7 @@

Functions

@@ -338,7 +338,7 @@

Functions

@@ -353,7 +353,7 @@

Functions

@@ -368,7 +368,7 @@

Functions

@@ -398,7 +398,7 @@

Functions

@@ -518,7 +518,7 @@

Functions

-
open fun onTimelineChanged(p0: Timeline, p1: Int)
+
open fun onTimelineChanged(p0: Timeline, p1: Int)
@@ -533,7 +533,7 @@

Functions

-
open fun onTracksChanged(p0: Tracks)
+
open fun onTracksChanged(p0: Tracks)
@@ -548,7 +548,7 @@

Functions

@@ -563,7 +563,7 @@

Functions

- +
diff --git a/api/media/media3-logging/com.google.android.horologist.media3.tracing/index.html b/api/media/media3-logging/com.google.android.horologist.media3.tracing/index.html index 0546cb8cbd..c9f6652ac8 100644 --- a/api/media/media3-logging/com.google.android.horologist.media3.tracing/index.html +++ b/api/media/media3-logging/com.google.android.horologist.media3.tracing/index.html @@ -78,7 +78,7 @@

Types

diff --git a/datalayer-helpers-guide/index.html b/datalayer-helpers-guide/index.html index ccdcefbcac..3c434bb658 100644 --- a/datalayer-helpers-guide/index.html +++ b/datalayer-helpers-guide/index.html @@ -1540,6 +1540,18 @@

Typical use cases:
wearAppHelper.markSetupComplete()
+
+

And when the app is no longer considered in a fully setup state, use the following:

+
wearAppHelper.markSetupNoLongerComplete()
+
+ + diff --git a/search/search_index.json b/search/search_index.json index 7f95fc23f6..6f930f914a 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Overview","text":"

Horologist is a group of libraries that aim to supplement Wear OS developers with features that are commonly required by developers but not yet available.

"},{"location":"#maintained-versions","title":"Maintained Versions","text":"

The currently maintained branches of Horologist are.

Version Branch Description 0.4.x release-0.4.x Wear Compose 1.2.x (stable) and Media3, and generally stable APIs. 0.5.x main Wear Compose 1.3 alpha, Media3 and generally latest alphas of Androidx."},{"location":"#media","title":"\ud83c\udfb5 Media","text":"

Horologist provides the Media Toolkit: a set of libraries to build Media apps on Wear OS and a sample app that you can run to see the toolkit in action.

The toolkit includes:

  • horologist-media-ui: common media UI components and screens like PlayerScreen.
  • horologist-media: domain model for Media related functionality. Provides an abstraction to the UI module (horologist-media-ui) that is agnostic to the Player implementation.
  • horologist-media-data: implementation of the domain module (horologist-media) using Media3.
  • horologist-media3-backend: Player on top of Media3 including functionalities such as avoiding playing music on the watch speaker.
  • media sample: sample app to listen to downloaded music.
Player Screen Browse Screen Entity Screen"},{"location":"#composables","title":"\ud83d\udcc5 Composables","text":"

High quality prebuilt composables, such as Time and Date pickers.

  • horologist-composables
DatePicker TimePickerWith12HourClock TimePicker SegmentedProgressIndicator SquareSegmentedProgressIndicator"},{"location":"#compose-layout","title":"\ud83d\udcd0 Compose Layout","text":"

Layout related functionality such as a Navigation Aware Scaffold.

  • horologist-compose-layout
fillMaxRectangle()"},{"location":"#compose-material","title":"\ud83d\udd32 Compose Material","text":"

Opinionated implementation of the components of the Wear Material Compose library , based on the specifications of Wear Material Design Kit .

  • horologist-compose-material
"},{"location":"#audio-and-ui","title":"\ud83d\udd0a Audio and UI","text":"

Domain model for Audio related functionality. Volume Control, Output switching. Subscribing to a Flow of changes in audio or output.

  • horologist-audio
  • horologist-audio-ui
VolumeScreen"},{"location":"#auth","title":"\ud83d\udd10 Auth","text":"

Libraries to help developers to build apps following the Sign-In guidelines for Wear OS .

  • horologist-auth-composables: composable screens for authentication use cases, with no dependency on the auth-data library.
  • horologist-auth-ui: composable screens for authentication use cases, with integration with the auth-data library
  • horologist-auth-data: implementation for Wear apps for most of the authentication methods listed in the Authentication on wearables guide.
  • horologist-auth-data-phone: implementation for Mobile apps for some of the authentication methods provided by the auth-data library.
  • sample wear: sample wear app to authenticate using different methods.
  • sample phone: sample phone app to authenticate using different methods.
"},{"location":"#tiles","title":"\u2630 Tiles","text":"

Kotlin coroutines flavoured TileService.

horologist-tiles

"},{"location":"#why-the-name","title":"Why the name?","text":"

The name mirrors the Accompanist name, and is also Watch related.

https://en.wiktionary.org/wiki/horologist

horologist (Noun) Someone who makes or repairs timepieces, watches or clocks.

"},{"location":"#contributions","title":"Contributions","text":"

Please contribute! We will gladly review any pull requests submitted. Make sure to read the Contributing page to know what our expectations of contributions are.

"},{"location":"#license","title":"License","text":"
Copyright 2023 The Android Open Source Project\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n
"},{"location":"audio-ui/","title":"Audio Settings UI library","text":""},{"location":"audio-ui/#volume-screen","title":"Volume Screen","text":"

A volume screen, showing the current audio output (headphones, speakers) and allowing to change the button with a stepper or bezel.

VolumeScreen(focusRequester = focusRequester)\n

"},{"location":"audio-ui/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-audio-ui:<version>\"\n}\n
"},{"location":"audio/","title":"Audio Settings Library","text":"

Domain model for Volume and Audio Output.

val audioRepository = SystemAudioRepository.fromContext(application)\n\naudioRepository.increaseVolume()\n\nval volumeState: StateFlow<VolumeState> = audioRepository.volumeState\n\nval audioOutput: StateFlow<AudioOutput> = audioRepository.audioOutput\n\nval output = audioOutput.value\nif (output is AudioOutput.BluetoothHeadset) {\n  println(output.name)\n}\n
"},{"location":"audio/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-audio:<version>\"\n}\n
"},{"location":"auth-composables/","title":"Auth Composables","text":"

This library contains a set of composables screens and components related to authentication.

The previews of the composables can be found in the debug folder of the module source code.

This library is not dependent on any specific authentication implementation as per architecture overview.

"},{"location":"auth-composables/#screens","title":"Screens","text":"
  • SignInPlaceholderScreen

  • SelectAccountScreen

  • CheckYourPhoneScreen

"},{"location":"auth-composables/#dialogs","title":"Dialogs","text":"
  • SignedInConfirmationDialog
"},{"location":"auth-composables/#chips","title":"Chips","text":"
  • CreateAccountChip

  • GuestModeChip

  • OtherOptionsChip

  • SignInChip

"},{"location":"auth-data-phone/","title":"Auth Data Phone","text":"

This library contains implementation for Mobile apps for some of the authentication methods provided by the auth-data library.

"},{"location":"auth-data-phone/#token-sharing","title":"Token sharing","text":"
  • TokenBundleRepository
    • TokenBundleRepositoryImpl
"},{"location":"auth-data/","title":"Auth Data","text":"

This library contains implementation for Wear apps for most of the authentication methods listed in the Authentication on wearables guide.

The repositories of this library are built mainly to support the components from auth-ui library, but can be used with your own UI components.

"},{"location":"auth-data/#token-sharing","title":"Token sharing","text":"
  • TokenBundleRepository
    • TokenBundleRepositoryImpl
"},{"location":"auth-data/#google-sign-in","title":"Google Sign-In","text":"
  • GoogleSignInAuthUserRepository
"},{"location":"auth-data/#oauth-pkce","title":"OAuth (PKCE)","text":"
  • PKCEConfigRepository
    • PKCEConfigRepositoryGoogleImpl
  • PKCEOAuthCodeRepository
    • PKCEOAuthCodeRepositoryImpl
  • PKCETokenRepository
    • PKCETokenRepositoryGoogleImpl
"},{"location":"auth-data/#oauth-device-grant","title":"OAuth (Device Grant)","text":"
  • DeviceGrantConfigRepository
    • DeviceGrantConfigRepositoryDefaultImpl
  • DeviceGrantTokenRepository
    • DeviceGrantTokenRepositoryGoogleImpl
  • DeviceGrantVerificationInfoRepository
    • DeviceGrantVerificationInfoRepositoryGoogleImpl
"},{"location":"auth-googlesignin-guide/","title":"Google Sign-In guide","text":"

This guide will walk you through on how to display a screen on your watch app so that users can select their Google account to sign-in to your app.

"},{"location":"auth-googlesignin-guide/#requirements","title":"Requirements","text":"

Follow the setup instructions for integrating Google Sign-in into an Android app from this link.

"},{"location":"auth-googlesignin-guide/#getting-started","title":"Getting started","text":"
  1. Add dependencies

    Add the following dependencies to your project\u2019s build.gradle:

    dependencies {\n    implementation \"com.google.android.horologist:horologist-auth-composables:<version>\"\n    implementation \"com.google.android.horologist:horologist-auth-ui:<version>\"\n    implementation \"com.google.android.horologist:horologist-compose-material:<version>\"\n}\n
  2. Create an instance of GoogleSignInClient

    Create an instance of GoogleSignInClient, according to your requirements, for example:

    val googleSignInClient = GoogleSignIn.getClient(\n    applicationContext,\n    GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)\n        .requestEmail()\n        .build()\n)\n
"},{"location":"auth-googlesignin-guide/#display-the-screen","title":"Display the screen","text":"
  1. Create a ViewModel

    Create your implementation of GoogleSignInViewModel, passing the GoogleSignInClient created:

    class MyGoogleSignInViewModel(\n    googleSignInClient: GoogleSignInClient,\n) : GoogleSignInViewModel(googleSignInClient)\n
  2. Display the screen

    Display the GoogleSignInScreen passing an instance of the GoogleSignInViewModel created:

    GoogleSignInScreen(\n   onAuthCancelled = { /* code to navigate to another screen on this event */ },\n   onAuthSucceed = { /* code to navigate to another screen on this event */ },\n   viewModel = hiltViewModel<MyGoogleSignInViewModel>()\n)\n

This sample uses Hilt to retrieve an instance of the ViewModel, but you should use what suits your project best, see this link for more info.

"},{"location":"auth-googlesignin-guide/#retrieve-the-signed-in-account","title":"Retrieve the signed in account","text":"

In order to have access an instance of the GoogleSignInAccount selected by the user, follow the steps:

  1. Implement GoogleSignInEventListener

    class GoogleSignInEventListenerImpl : GoogleSignInEventListener {\n    override suspend fun onSignedIn(account: GoogleSignInAccount) {\n        // your implementation using the account parameter\n    }\n}\n
  2. Pass the listener to the ViewModel

    Pass an instance of GoogleSignInEventListener to GoogleSignInViewModel:

    class MyGoogleSignInViewModel(\n googleSignInClient: GoogleSignInClient,\n googleSignInEventListener: GoogleSignInEventListener,\n) : GoogleSignInViewModel(googleSignInClient, googleSignInEventListener)\n
"},{"location":"auth-overview/","title":"Auth libraries","text":""},{"location":"auth-overview/#overview","title":"Overview","text":"

The purpose of the auth libraries is to:

  • help developers to build apps following the Sign-In guidelines for Wear OS;
  • provide implementation (for Wear and Mobile) for most of the authentication methods listed in the Authentication on wearables guide;

The following libraries are provided:

  • auth-composables: composable screens for Authentication use cases, with no dependency on the auth-data library.
  • auth-ui: composable screens for Authentication use cases, with integration with the auth-data library.
  • auth-data: implementation for Wear apps for most of the authentication methods listed in the Authentication on wearables guide.
  • auth-data-phone: implementation for Mobile apps for some of the authentication methods provided by the auth-data library.

The following sample apps are also provided:

  • auth-sample-wear: sample wear app to authenticate using different methods.
  • auth-sample-phone: sample phone app to authenticate using different methods.
"},{"location":"auth-overview/#architecture-overview","title":"Architecture overview","text":"

The auth libraries are separated by layers (UI and data), following the recommended app architecture. The reason for including an extra UI library (auth-composables) is to provide flexibility to projects that would like to only use the UI components that are not dependent on auth-data.

"},{"location":"auth-overview/#getting-started","title":"Getting started","text":"

The usage of the auth libraries will vary according to the requirements of your project.

As per architecture overview, your project might not need to add all the auth libraries as dependency. If that\u2019s the case, refer to the documentation of each library required to your project for a guide on how to get started.

"},{"location":"auth-sample-apps/","title":"Auth sample apps","text":""},{"location":"auth-sample-apps/#wear-sample","title":"Wear sample","text":"

The app showcases the implementation of the following authentication methods:

  • Token sharing
  • OAuth (PKCE)
  • OAuth (Device Grant)
  • Google Sign-In
"},{"location":"auth-sample-apps/#phone-sample","title":"Phone sample","text":"

The app showcases the implementation of the following authentication methods:

  • Token sharing
"},{"location":"auth-tokenshare-guide/","title":"Token sharing guide","text":"

This guide will walk you through on how to securely transfer authentication data from the phone app to the watch app using Horologist's Auth libraries.

"},{"location":"auth-tokenshare-guide/#requirements","title":"Requirements","text":"

Horologist Auth library is built on top of Wearable Data Layer API, so your phone and watch apps must:

  • have APK signatures and the signature schemes identical;
  • the same package name;
"},{"location":"auth-tokenshare-guide/#getting-started","title":"Getting started","text":"
  1. Add dependencies

    Add the following dependencies to your project\u2019s build.gradle.

    For the phone app project:

    dependencies {\n    implementation \"com.google.android.horologist:horologist-auth-data-phone:<version>\"\n    implementation \"com.google.android.horologist:horologist-datalayer:<version>\"\n}\n

    For the watch app project:

    dependencies {\n    implementation \"com.google.android.horologist:horologist-auth-data:<version>\"\n    implementation \"com.google.android.horologist:horologist-datalayer:<version>\"\n}\n
  2. Add capability to phone app project

    On the phone app project, add a wear.xml file in the res/values folder with the following content:

    <resources>\n    <string-array name=\"android_wear_capabilities\">\n        <item>horologist_phone</item>\n    </string-array>\n</resources>\n
  3. Create a WearDataLayerRegistry

    In both projects, create an instance of WearDataLayerRegistry from the datalayer:

    val registry = WearDataLayerRegistry.fromContext(\n    application = // application context,\n    coroutineScope = // a coroutine scope\n)\n

    This class should be created as a singleton in your app.

  4. Define the data to be transferred

    Define which authentication data that should be transferred from the phone to the watch. It can be a data class with many properties, it can also be a protocol buffer. For this guide, we will pass a simple String instance.

  5. Create a Serializer for the data

    Create a DataStore Serializer class for the class defined to be transferred from the phone to the watch (String for this guide):

    public object TokenSerializer : Serializer<String> {\n    override val defaultValue: String = \"\"\n\n    override suspend fun readFrom(input: InputStream): String =\n        InputStreamReader(input).readText()\n\n    override suspend fun writeTo(t: String, output: OutputStream) {\n        withContext(Dispatchers.IO) {\n            output.write(t.toByteArray())\n        }\n    }\n}   \n

    This class should preferable be placed in a shared module between the phone and watch projects, but could also be duplicated in both projects.

    More information about this serialization in this blog post.

"},{"location":"auth-tokenshare-guide/#send-authentication-data-from-the-phone","title":"Send authentication data from the phone","text":"
  1. Create a TokenBundleRepository on the phone project

    Create an instance of TokenBundleRepository on the phone app project:

    val tokenBundleRepository = TokenBundleRepositoryImpl(\n    registry = registry,\n    coroutineScope = // a coroutine scope,\n    serializer = TokenSerializer\n)   \n
  2. Check if the repository is available (optional)

    Before using the repository, you can check if it is available to be used on the current device with:

    tokenBundleRepository.isAvailable()\n

    If the repository is not available on the device, all the calls to it will fail silently.

    See the requirements of Wearable Data Layer API.

  3. Send authentication data

    The authentication data can be sent from the phone calling update:

    tokenBundleRepository.update(\"token\")\n
"},{"location":"auth-tokenshare-guide/#receive-authentication-data-on-the-watch","title":"Receive authentication data on the watch","text":"
  1. Create a TokenBundleRepository on the watch project

    Create an instance of TokenBundleRepository on the watch app project:

    val tokenBundleRepository = TokenBundleRepositoryImpl.create(\n    registry = registry,\n    serializer = TokenSerializer\n)\n
  2. Receive authentication data

    The authentication data can be listened from the watch via the flow property:

    tokenBundleRepository.flow\n

"},{"location":"auth-ui/","title":"Auth UI","text":"

This library contains a set of composables screens and components related to authentication.

The previews of the composables can be found in the debug folder of the module source code.

The composables of this module might depend on repository interfaces defined in auth-data library. The implementation of these repositories does not necessarily need to be from auth-data, they can be your own implementation. Some of the composables might depend on an external library.

"},{"location":"auth-ui/#screens","title":"Screens","text":""},{"location":"auth-ui/#common","title":"Common","text":""},{"location":"auth-ui/#signinpromptscreen","title":"SignInPromptScreen","text":"

A screen to prompt users to sign in.

It helps achieve to the following best practices:

  • Explain sign-in benefits
  • Provide alternatives
"},{"location":"auth-ui/#google-sign-in","title":"Google Sign-In","text":""},{"location":"auth-ui/#googlesigninscreen","title":"GoogleSignInScreen","text":"

A screen for the Google Sign-In authentication method.

It uses different screens from auth-composables to display the full authentication flow.

It relies on the Google Sign-In for Android library for authentication and account selection. So an instance of GoogleSignInClient has to be provided to GoogleSignInViewModel.

"},{"location":"auth-ui/#oauth","title":"OAuth","text":""},{"location":"auth-ui/#pkcesigninscreen","title":"PKCESignInScreen","text":"

A screen for the OAuth (PKCE) authentication method.

It uses different screens from auth-composables to display the full authentication flow.

A implementation for the following repositories are required to be provided:

  • PKCEConfigRepository
  • PKCEOAuthCodeRepository
  • PKCETokenRepository
"},{"location":"auth-ui/#devicegrantsigninscreen","title":"DeviceGrantSignInScreen","text":"

A screen for the OAuth (Device Grant) authentication method.

It uses different screens from auth-composables to display the full authentication flow.

A implementation for the following repositories are required to be provided:

  • DeviceGrantConfigRepository
  • DeviceGrantVerificationInfoRepository
  • DeviceGrantTokenRepository
"},{"location":"composables/","title":"Composables library","text":""},{"location":"composables/#date-picker","title":"Date Picker","text":""},{"location":"composables/#segmented-progress-indicator","title":"Segmented Progress Indicator","text":""},{"location":"composables/#time-pickers","title":"Time Pickers","text":""},{"location":"composables/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-composables:<version>\"\n}\n
"},{"location":"compose-layout/","title":"Compose Layout library","text":""},{"location":"compose-layout/#scalinglazycolumn-responsive-layout","title":"ScalingLazyColumn responsive() layout.","text":"

The responsive() layout factory will ensure that your ScalingLazyColumn is positioned correctly on all screen sizes.

Pass in a boolean for the firstItemIsFullWidth param to indicate whether the first item can fit just below TimeText, or must be shifted down further to avoid cutting off the edges.

The overloaded ScalingLazyColumn composable with ScalingLazyColumnState param, when combined with responsive() will handle all the following:

  • Position the first item near the top on all screen sizes.
  • Ensure the last item can be scrolled into view.
  • Handle RSB/Bezel scrolling with Fling.
  • Size side margins based on a percentage, adapting to different screen sizes.
val columnState =\n    rememberColumnState(ScalingLazyColumnDefaults.responsive(firstItemIsFullWidth = false))\n\nScaffold(\n    modifier = Modifier\n        .fillMaxSize(),\n    timeText = {\n        TimeText(modifier = Modifier.scrollAway(columnState))\n    }\n) {\n    ScalingLazyColumn(\n        modifier = Modifier.fillMaxSize(),\n        columnState = columnState,\n    ) {\n        item {\n            ListHeader {\n                Text(\n                    text = \"Main\",\n                    modifier = Modifier.fillMaxWidth(0.6f),\n                    textAlign = TextAlign.Center\n                )\n            }\n        }\n        items(10) {\n            Chip(\"Item $it\", onClick = {})\n        }\n    }\n}\n
"},{"location":"compose-layout/#navigation-scaffold","title":"Navigation Scaffold.","text":"

Syncs the TimeText, PositionIndicator and Scaffold to the current navigation destination state. The TimeText will scroll out of the way of content automatically.

WearNavScaffold(\n    startDestination = \"home\",\n    navController = navController\n) {\n    scalingLazyColumnComposable(\n        \"home\",\n        scrollStateBuilder = { ScalingLazyListState(initialCenterItemIndex = 0) }\n    ) {\n        MenuScreen(\n            scrollState = it.scrollableState,\n            focusRequester = it.viewModel.focusRequester\n        )\n    }\n\n    scalingLazyColumnComposable(\n        \"items\",\n        scrollStateBuilder = { ScalingLazyListState() }\n    ) {\n        ScalingLazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .scrollableColumn(it.viewModel.focusRequester, it.scrollableState),\n            state = it.scrollableState\n        ) {\n            items(100) {\n                Text(\"i = $it\")\n            }\n        }\n    }\n\n    scrollStateComposable(\n        \"settings\",\n        scrollStateBuilder = { ScrollState(0) }\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .verticalScroll(state = it.scrollableState)\n                .scrollableColumn(focusRequester = it.viewModel.focusRequester, scrollableState = it.scrollableState),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            (1..100).forEach {\n                Text(\"i = $it\")\n            }\n        }\n    }\n}\n
"},{"location":"compose-layout/#box-inset-layout","title":"Box Inset Layout.","text":"

Use as a break glass for simple layout to fit within a safe square.

Box(\n    modifier = Modifier\n        .fillMaxRectangle()\n) {\n    // App Content here        \n}\n

"},{"location":"compose-layout/#fade-away-modifier","title":"Fade Away Modifier","text":""},{"location":"compose-layout/#ambientaware-composable","title":"AmbientAware composable","text":"

AmbientAware allows your UI to react to ambient mode changes. For more information on how Ambient mode and Always-on work on Wear OS, see the developer guidance.

You should place this composable high up in your design, as it alters the behavior of the Activity.

@Composable\nfun WearApp() {\n    AmbientAware { ambientStateUpdate ->\n        // App Content here\n    }\n}\n

If you need some screens to use always-on, and others not to, then you can use the additional argument supplied to AmbientAware.

For example, in a workout app, it is desirable that the main workout screen uses always-on, but the workout summary at the end does not. See the ExerciseClient guide and samples for more information on building a workout app.

@Composable\nfun WearApp() {\n    // Hoist state here for your current screen logic\n\n    AmbientAware(isAlwaysOnScreen = currentScreen.useAlwaysOn) { ambientStateUpdate ->\n        // App Content here\n    }\n}\n
"},{"location":"compose-layout/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-compose-layout:<version>\"\n}\n
"},{"location":"compose-material/","title":"Compose Material","text":"

A library providing opinionated implementation of the components of the Wear Material Compose library, based on the specifications of Wear Material Design Kit.

"},{"location":"compose-material/#motivation","title":"Motivation","text":"

In order to display a Chip component in a Wear OS app, using Wear Material Compose library, the code would look like this:

Chip(\n    label = { Text(\"Primary label\") },\n    onClick = { },\n    secondaryLabel = { Text(\"Secondary label\") },\n    icon = { Icon(imageVector = Icons.Default.Image, contentDescription = null) }\n)\n

In comparison, using Horologist's Compose Material library, the code would look simpler:

Chip(\n    label = \"Primary label\",\n    onClick = { },\n    secondaryLabel = \"Secondary label\",\n    icon = Icons.Default.Image\n)\n

As seen above, Horologist's Compose Material provides convenient ways of passing parameters to the components. Furthermore, for this particular component, it will also take care of:

  • Define the maximum number of lines for each label, truncating them appropriately, even when the user has changed the font size in the OS settings;
  • Change the text alignment and maximum number of lines of the primary label when the secondary label is not present;
  • Adjust the content padding based on the icon size;

The list is not exhaustive and it varies for each individual component.

"},{"location":"compose-material/#when-this-library-should-not-be-used","title":"When this library should not be used?","text":"

If the specifications for the component needed in your app does not match the specifications of the components listed in Wear Material Design Kit, then Wear Material Compose library should be used instead.

"},{"location":"compose-tools/","title":"Compose Tools library","text":""},{"location":"compose-tools/#tile-previews","title":"Tile Previews.","text":"

Android Studio Preview support for tiles, using the TilesRenderer inside and AndroidView. Uses either raw Tiles proto, or the TilesLayoutRenderer abstraction to define a predictable process for generating a Tile for a given state.

@WearPreviewDevices\n@WearPreviewFontSizes\n@Composable\nfun SampleTilePreview() {\n    val context = LocalContext.current\n\n    val tileState = remember { SampleTileRenderer.TileState(0) }\n\n    val resourceState = remember {\n        val image =\n            BitmapFactory.decodeResource(context.resources, R.drawable.ic_uamp).toImageResource()\n        SampleTileRenderer.ResourceState(image)\n    }\n\n    val renderer = remember {\n        SampleTileRenderer(context)\n    }\n\n    TileLayoutPreview(\n        tileState,\n        resourceState,\n        renderer\n    )\n}\n

"},{"location":"compose-tools/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-compose-tools:<version>\"\n}\n
"},{"location":"contributing/","title":"How to Contribute","text":"

We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow.

If you find a common problem that you think would help other Wear developers please consider submitting a PR. Please avoid significant work before raising an issue https://github.com/google/horologist/issues with the label \"Feature Request\"

"},{"location":"contributing/#development","title":"Development","text":"

The project should work immediately from a fresh checkout in Android Studio (Stable or newer) or Gradle (./gradlew).

When submitting a PR, please check API compatibility and lint rules first.

A good first step is

./gradlew spotlessApply spotlessCheck compileDebugSources compileReleaseSources metalavaGenerateSignature metalavaGenerateSignatureRelease lintDebug\n

Also make sure you have (Git LFS) installed.

If you change any code affecting screenshot tests, then run the following to update changed images on a Linux host. Alternatively uncomment the same property in gradle.properties.

./gradlew testDebug -P screenshot.record=repair\n

This can be automated For a PR against the same branch, the Github action should commit against PR if possible. For maintainers, this means creating the branch in the google/horologist repo. For contributors, once your PR fails, create a second PR in your fork, that will fix the issue.

"},{"location":"contributing/#contributor-license-agreement","title":"Contributor License Agreement","text":"

Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution, this simply gives us permission to use and redistribute your contributions as part of the project. Head over to https://cla.developers.google.com/ to see your current agreements on file or to sign a new one.

You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again.

"},{"location":"contributing/#code-reviews","title":"Code reviews","text":"

All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult GitHub Help for more information on using pull requests.

"},{"location":"contributing/#translation-and-localization","title":"Translation and localization","text":"

This project uses a semi-automatic pipeline to translate strings. When new or updated localized strings are ready, a PR is generated (example: google/horologist#692). Only the files configured via localization.bzl are sent for translation.

If you see a problem with translated text, don't edit localized resource files (e.g. res/values-en/strings.xml) manually, as they'll be overwritten. Instead, file an issue and use the l10n label. This will then be forwarded to the relevant teams.

"},{"location":"contributing/#project-direction-and-ownership","title":"Project Direction and Ownership","text":"

There are a couple of reasons we may not accept an otherwise valuable contribution.

  • Where the internal framework feature team, thinks the contribution is against the long term direction of the library.
  • Where long term ownership is unclear, such as a large contribution that likely involves ongoing maintenance.
"},{"location":"datalayer-helpers-guide/","title":"DataLayer helpers libraries","text":"

These libraries provides an easy means to detect and install your app across both watch and phone.

However, they are not intended to cover complex use cases, or complex interactions between watch and phone.

"},{"location":"datalayer-helpers-guide/#getting-started","title":"Getting started","text":"
  1. Include the necessary dependency:

    dependencies {\n    implementation \"com.google.android.horologist:horologist-datalayer-watch:<version>\"\n}\n

    and

    dependencies {\n    implementation \"com.google.android.horologist:horologist-datalayer-phone:<version>\"\n}\n

    For your watch and phone projects respectively.

  2. Initialize the client, including passing a WearDataLayerRegistry.

    val appHelper = WearDataLayerAppHelper(context, wearDataLayerRegistry, scope)\n\n// or\nval appHelper = PhoneDataLayerAppHelper(context, wearDataLayerRegistry)\n
"},{"location":"datalayer-helpers-guide/#typical-use-cases","title":"Typical use cases:","text":"
  1. Connection and installation status

    This is something that your app may do from time to time, or on start up.

    val connectedNodes = appHelper.connectedNodes()\n

    The resulting list might will contain entries such as:

    AppHelperNodeStatus(\n    id=7cd1c38a,\n    displayName=Google Pixel Watch,\n    appInstallationStatus=Installed(nodeType=WATCH),\n    surfacesInfo=# SurfacesInfo@125fcbff\n        complications {\n            instance_id: 1234\n            name: \"MyComplication\"\n            timestamp {\n                nanos: 738000000\n                seconds: 1680015523\n            }\n            type: \"SHORT_TEXT\"\n        }\n        tiles {\n            name: \"MyTile\"\n            timestamp {\n                nanos: 364000000\n                seconds: 1680016845\n            }\n        }\n)\n
  2. Responding to availability change

    Once you've established the app on both devices, you may wish to respond to when the partner device connects or disconnects. For example, you may only want to show a \"launch workout\" button on the phone when the watch is connected.

    val nodes by appHelper.connectedAndInstalledNodes\n    .collectAsStateWithLifecycle()\n
  3. Installing the app on the other device

    Where the app isn't installed on the other device - be that phone or watch - then the library offers a one step option to launch installation:

    appHelper.installOnNode(node.id)\n
  4. Launching the app on the other device

    If the app is installed on the other device, you can launch it remotely:

    val result = appHelper.startRemoteOwnApp(node.id)\n
  5. Launching a specific activity on the other device

    In addition to launching your own app, you may wish to launch a different activity as part of the user journey:

    val config = activityConfig { \n    packageName = \"com.example.myapp\"\n    classFullName = \"com.example.myapp.MyActivity\"\n}\nappHelper.startRemoteActivity(node.id, config)\n
  6. Launching the companion app

    In some cases, it can be useful to launch the companion app, either from the watch or the phone.

    For example, if the connected device does not have your Tile installed, you may wish to offer the user the option to navigate to the companion app to install it:

    if (node.surfacesInfo.tilesList.isEmpty() && askUserAttempts < MAX_ATTEMPTS) {\n    // Show guidance to the user and then launch companion\n    // to allow the to install the Tile.\n    val result = appHelper.startCompanion(node.id)\n}\n
  7. Tracking Tile installation (Wear-only)

    To determine whether your Tile(s) are installed, add the following to your TileService:

    In onTileAddEvent:

    wearAppHelper.markTileAsInstalled(\"SummaryTile\")\n

    In onTileRemoveEvent:

    wearAppHelper.markTileAsRemoved(\"SummaryTile\")\n
  8. Tracking Complication installation (Wear-only)

    To determine whether your Complication(s) are in-use, add the following to your ComplicationDataSourceService:

    In onComplicationActivated:

    wearAppHelper.markComplicationAsActivated(\"GoalsComplication\")\n

    In onComplicationDeactivated:

    wearAppHelper.markComplicationAsDeactivated(\"GoalsComplication\")\n
  9. Tracking the main activity has been launched at least once (Wear-only)

To determine if your main activity has been launched once, use:

kotlin wearAppHelper.markActivityLaunchedOnce()

"},{"location":"datalayer/","title":"DataLayer library","text":"

DataStore documentation https://developer.android.com/topic/libraries/architecture/datastore

Direct DataLayer sample code https://github.com/android/wear-os-samples

"},{"location":"datalayer/#datalayer-approach","title":"DataLayer approach.","text":"

The Horologist DataLayer libraries, provide common abstractions on top of the Wearable DataLayer. These are built upon a common assumption of Google Protobuf and gRPC, which allows sharing data definitions throughout your Wear and Mobile apps.

See this gradle build file for an example of configuring a build to use proto definitions.

syntax = \"proto3\";\n\nmessage CounterValue {\n  int64 value = 1;\n  .google.protobuf.Timestamp updated = 2;\n}\n\nmessage CounterDelta {\n  int64 delta = 1;\n}\n\nservice CounterService {\n  rpc getCounter(.google.protobuf.Empty) returns (CounterValue);\n  rpc increment(CounterDelta) returns (CounterValue);\n}\n
"},{"location":"datalayer/#registering-serializers","title":"Registering Serializers.","text":"

The WearDataLayerRegistry is an application singleton to register the Serializers.

object CounterValueSerializer : Serializer<CounterValue> {\n    override val defaultValue: CounterValue\n        get() = CounterValue.getDefaultInstance()\n\n    override suspend fun readFrom(input: InputStream): CounterValue =\n        try {\n            CounterValue.parseFrom(input)\n        } catch (exception: InvalidProtocolBufferException) {\n            throw CorruptionException(\"Cannot read proto.\", exception)\n        }\n\n    override suspend fun writeTo(t: CounterValue, output: OutputStream) {\n        t.writeTo(output)\n    }\n}\n\nval registry = WearDataLayerRegistry.fromContext(\n    application = sampleApplication,\n    coroutineScope = coroutineScope,\n).apply {\n    registerSerializer(CounterValueSerializer)\n}\n
"},{"location":"datalayer/#use-androidx-datastore","title":"Use Androidx DataStore","text":"

This library provides a new implementation of Androidx DataStore, in addition to the local Proto and Preferences implementations. The implementation uses the Wearable DataClient with a single owner and multiple readers.

See DataStore.

"},{"location":"datalayer/#publishing-a-datastore","title":"Publishing a DataStore","text":"
private val dataStore: DataStore<CounterValue> by lazy {\n  registry.protoDataStore<CounterValue>(lifecycleScope)\n}\n
"},{"location":"datalayer/#reading-a-remote-datastore","title":"Reading a remote DataStore","text":"
val counterFlow = registry.protoFlow<CounterValue>(TargetNodeId.PairedPhone)\n
"},{"location":"datalayer/#using-grpc","title":"Using gRPC","text":"

This library implements the gRPC transport over the Wearable MessageClient using the RPC request feature.

"},{"location":"datalayer/#implementing-a-service","title":"Implementing a service","text":"
class CounterService(val dataStore: DataStore<GrpcDemoProto.CounterValue>) :\n    CounterServiceGrpcKt.CounterServiceCoroutineImplBase() {\n        override suspend fun getCounter(request: Empty): GrpcDemoProto.CounterValue {\n            return dataStore.data.first()\n        }\n\n        override suspend fun increment(request: GrpcDemoProto.CounterDelta): GrpcDemoProto.CounterValue {\n            return dataStore.updateData {\n                it.copy {\n                    this.value = this.value + request.delta\n                    this.updated = System.currentTimeMillis().toProtoTimestamp()\n                }\n            }\n        }\n    }\n\nclass WearCounterDataService : BaseGrpcDataService<CounterServiceGrpcKt.CounterServiceCoroutineImplBase>() {\n\n    private val dataStore: DataStore<CounterValue> by lazy {\n        registry.protoDataStore<CounterValue>(lifecycleScope)\n    }\n\n    override val registry: WearDataLayerRegistry by lazy {\n        WearDataLayerRegistry.fromContext(\n            application = applicationContext,\n            coroutineScope = lifecycleScope,\n        ).apply {\n            registerSerializer(CounterValueSerializer)\n        }\n    }\n\n    override fun buildService(): CounterServiceGrpcKt.CounterServiceCoroutineImplBase {\n        return CounterService(dataStore)\n    }\n}\n
"},{"location":"datalayer/#calling-a-remote-service","title":"Calling a remote service","text":"
val client = registry.grpcClient(\n    nodeId = TargetNodeId.PairedPhone,\n    coroutineScope = sampleApplication.servicesCoroutineScope,\n) {\n    CounterServiceGrpcKt.CounterServiceCoroutineStub(it)\n}\n\n// Call the increment method from the proto service definition\nval newValue: CounterValue =\n    counterService.increment(counterDelta { delta = i.toLong() })\n
"},{"location":"datalayer/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-datalayer:<version>\"\n}\n
"},{"location":"media-data/","title":"Media Data library","text":"

This library contains the implementation of the repositories defined in the media domain library, using Media3 and an internal database as data sources.

It also exposes its data sources classes so they can be used by your custom repositories.

"},{"location":"media-data/#mediadownloadservice","title":"MediaDownloadService","text":"

An implementation of Media3\u2019s DownloadService that, in conjunction with auxiliary classes, will monitor the state and progress of media downloads, and update the information in the internal database.

"},{"location":"media-data/#usage","title":"Usage","text":"
  1. Add your own implementation of the service, extending MediaDownloadService;

  2. Add your service implementation to your app\u2019s AndroidManifest.xml:

    <service android:name=\"MediaDownloadServiceImpl\"\n    android:exported=\"false\">\n    <intent-filter>\n        <action android:name=\"com.google.android.exoplayer.downloadService.action.RESTART\"/>\n        <category android:name=\"android.intent.category.DEFAULT\"/>\n    </intent-filter>\n</service>\n

"},{"location":"media-data/#media-toolkit-implementation","title":"Media Toolkit implementation","text":""},{"location":"media-data/#downloadmanagerlistener","title":"DownloadManagerListener","text":"

The DownloadManagerListener is an implementation of listener for DownloadManager events which can also get notified of DownloadService creation and destruction events. This class persists information such as the id and the download status in the local database MediaDownloadLocalDataSource.

override fun onDownloadChanged(\n        downloadManager: DownloadManager,\n        download: Download,\n        finalException: Exception?\n    ) {\n        coroutineScope.launch {\n            val mediaId = download.request.id\n            val status = MediaDownloadEntityStatusMapper.map(download.state)\n\n            if (status == MediaDownloadEntityStatus.Downloaded) {\n                mediaDownloadLocalDataSource.setDownloaded(mediaId)\n            } else {\n                mediaDownloadLocalDataSource.updateStatus(mediaId, status)\n            }\n        }\n\n        downloadProgressMonitor.start(downloadManager)\n    }\n\noverride fun onDownloadRemoved(downloadManager: DownloadManager, download: Download) {\n        coroutineScope.launch {\n            val mediaId = download.request.id\n            mediaDownloadLocalDataSource.delete(mediaId)\n        }\n    }\n

"},{"location":"media-data/#downloadprogressmonitor","title":"DownloadProgressMonitor","text":"

The DownloadProgressMonitor monitors the status of the download by polling the DownloadManager and persists the progress in the local database MediaDownloadLocalDataSource.

    private fun update(downloadManager: DownloadManager) {\n        coroutineScope.launch {\n            val downloads = mediaDownloadLocalDataSource.getAllDownloading()\n\n            if (downloads.isNotEmpty()) {\n                for (it in downloads) {\n                    downloadManager.downloadIndex.getDownload(it.mediaId)?.let { download ->\n                        mediaDownloadLocalDataSource.updateProgress(\n                            mediaId = download.request.id,\n                            progress = download.percentDownloaded\n                                .coerceAtLeast(DOWNLOAD_PROGRESS_START),\n                            size = download.contentLength\n                        )\n                    }\n                }\n            } else {\n                stop()\n            }\n        }\n\n        if (running) {\n            handler.removeCallbacksAndMessages(null)\n            handler.postDelayed({ update(downloadManager) }, UPDATE_INTERVAL_MILLIS)\n        }\n    }\n
"},{"location":"media-data/#ui-implementation","title":"UI Implementation","text":"

The PlaylistsDownloadScreen is composed by the MediaContent and the ButtonContent. The MediaContent displays the content that is being downloaded or already downloaded. For content that is being downloaded, we display the download progress for each track in place of the artist name in the secondaryLabel

val secondaryLabel = when (downloadMediaUiModel) {\n        is DownloadMediaUiModel.Downloading -> {\n            when (downloadMediaUiModel.progress) {\n                is DownloadMediaUiModel.Progress.Waiting -> stringResource(\n                    id = R.string.horologist_playlist_download_download_progress_waiting\n                )\n\n                is DownloadMediaUiModel.Progress.InProgress -> when (downloadMediaUiModel.size) {\n                    is DownloadMediaUiModel.Size.Known -> {\n                        val size = Formatter.formatShortFileSize(\n                            LocalContext.current,\n                            downloadMediaUiModel.size.sizeInBytes\n                        )\n                        stringResource(\n                            id = R.string.horologist_playlist_download_download_progress_known_size,\n                            downloadMediaUiModel.progress.progress,\n                            size\n                        )\n                    }\n\n                    DownloadMediaUiModel.Size.Unknown -> stringResource(\n                        id = R.string.horologist_playlist_download_download_progress_unknown_size,\n                        downloadMediaUiModel.progress.progress\n                    )\n                }\n            }\n        }\n\n        is DownloadMediaUiModel.Downloaded -> downloadMediaUiModel.artist\n        is DownloadMediaUiModel.NotDownloaded -> downloadMediaUiModel.artist\n    }\n

The ButtonContent displays either a download chip or three buttons to delete, shuffle and play the content. While content is being downloaded, we show an animation on the first button on the left to track progress .

            if (state.downloadMediaListState == PlaylistDownloadScreenState.Loaded.DownloadMediaListState.None) {\n                if (state.downloadsProgress is DownloadsProgress.InProgress) {\n                    StandardChip(\n                        label = stringResource(id = R.string.horologist_playlist_download_button_cancel),\n                        onClick = { onCancelDownloadButtonClick(state.collectionModel) },\n                        modifier = Modifier.padding(bottom = 16.dp),\n                        icon = Icons.Default.Close\n                    )\n                } else {\n                    StandardChip(\n                        label = stringResource(id = R.string.horologist_playlist_download_button_download),\n                        onClick = { onDownloadButtonClick(state.collectionModel) },\n                        modifier = Modifier.padding(bottom = 16.dp),\n                        icon = Icons.Default.Download\n                    )\n                }\n            } else {\n                Row(\n                    modifier = Modifier\n                        .padding(bottom = 16.dp)\n                        .height(52.dp),\n                    verticalAlignment = CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(6.dp, CenterHorizontally)\n                ) {\n                    FirstButton(\n                        downloadMediaListState = state.downloadMediaListState,\n                        downloadsProgress = state.downloadsProgress,\n                        collectionModel = state.collectionModel,\n                        onDownloadButtonClick = onDownloadButtonClick,\n                        onCancelDownloadButtonClick = onCancelDownloadButtonClick,\n                        onDownloadCompletedButtonClick = onDownloadCompletedButtonClick,\n                        modifier = Modifier\n                            .weight(weight = 0.3F, fill = false)\n                    )\n\n                    StandardButton(\n                        imageVector = Icons.Default.Shuffle,\n                        contentDescription = stringResource(id = R.string.horologist_playlist_download_button_shuffle_content_description),\n                        onClick = { onShuffleButtonClick(state.collectionModel) },\n                        modifier = Modifier\n                            .weight(weight = 0.3F, fill = false)\n                    )\n\n                    StandardButton(\n                        imageVector = Icons.Filled.PlayArrow,\n                        contentDescription = stringResource(id = R.string.horologist_playlist_download_button_play_content_description),\n                        onClick = { onPlayButtonClick(state.collectionModel) },\n                        modifier = Modifier\n                            .weight(weight = 0.3F, fill = false)\n                    )\n                }\n            }\n
"},{"location":"media-data/#result","title":"Result","text":"

Download screens:

"},{"location":"media-playerscreen/","title":"Stateful PlayerScreen guide","text":"

This is a guide on how to use the stateful PlayerScreen with your own implementation of PlayerRepository.

"},{"location":"media-playerscreen/#basic-usage","title":"Basic usage","text":""},{"location":"media-playerscreen/#1-implement-playerrepository","title":"1. Implement PlayerRepository","text":"
class PlayerRepositoryImpl : PlayerRepository {\n    // implement required properties and functions\n}\n

In the sample implementation below, the repository listens to the events of Media3's Player and update its property values accordingly (see onIsPlayingChanged). Its operations are also called on the Player (see setPlaybackSpeed).

class PlayerRepositoryImpl(\n    private val player: Player\n) : PlayerRepository {\n\n    private val _currentState: MutableStateFlow<PlayerState> = MutableStateFlow(PlayerState.Idle)\n    override val currentState: StateFlow<PlayerState> = _currentState\n\n    private val listener = object : Player.Listener {\n\n        override fun onIsPlayingChanged(isPlaying: Boolean) {\n            _currentState.value = if (isPlaying) { \n                PlayerState.Playing\n            } else {\n                PlayerState.Ready\n            }\n        }\n    }\n\n    init {\n        player.add(listener)\n    }\n\n    fun setPlaybackSpeed(speed: Float) { \n        player.setPlaybackSpeed(speed) \n    }\n}\n
"},{"location":"media-playerscreen/#2-extend-playerviewmodel","title":"2. Extend PlayerViewModel","text":"

Pass your implementation of PlayerRepository as constructor parameter.

class MyCustomViewModel(\n    playerRepository: PlayerRepositoryImpl\n): PlayerViewModel(playerRepository) {\n    // add custom implementation\n}\n
"},{"location":"media-playerscreen/#3-add-playerscreen","title":"3. Add PlayerScreen","text":"

Pass your PlayerViewModel extension as value to the constructor parameter.

PlayerScreen(playerViewModel = myCustomViewModel)\n
"},{"location":"media-playerscreen/#class-diagram","title":"Class diagram","text":"

The following diagram shows the interactions between the classes.

"},{"location":"media-sample/","title":"Media sample app","text":"

The goal of this sample is to show how to implement an audio media app for Wear OS, using the Horologist media libraries, following the design principles described in Considerations for media apps .

The app supports listening to downloaded music. It loads a music catalog from a remote server and allows the user to browse the albums and songs. Tapping on a song will play it through connected speakers or headphones. Under the hood it uses Media3.

"},{"location":"media-sample/#features","title":"Features","text":"

The app showcases the implementation of the following features:

  • Media playback, restricted to paired Bluetooth devices
  • Launch of Bluetooth settings to connect devices for media playback
  • Volume control
  • Radial background based on media artwork color palette
  • Download media
  • API sync with WorkManager
  • Network rules
  • Splash screen
  • Marquee text for song titles
  • Tiles

This list is not exhaustive.

"},{"location":"media-sample/#audio","title":"Audio","text":"

Music provided by the Free Music Archive.

  • Wake Up by The Kyoto Connection.

Recordings provided by the Ambisonic Sound Library.

  • Pre Game Marching Band by Watson Wu
  • Chickens on a Farm by Watson Wu
  • Rural Market Busker by Stephan Schutze
  • Steamtrain Interior by Stephan Schutze
  • Rural Road Car Pass by Stephan Schutze
  • 10 Feet from Shore by Watson Wu
"},{"location":"media-toolkit/","title":"Media Toolkit","text":""},{"location":"media-toolkit/#overview","title":"Overview","text":"

Horologist provides what it is called the \"Media Toolkit\": a set of libraries to build media apps on Wear OS and a sample app that you can run to see the toolkit in action.

The following modules in the Horologist project are part of the toolkit:

  • media-ui: common media UI components and screens like PlayerScreen.
  • media: domain model for media related functionality. Provides an abstraction to the UI module (media-ui) that is agnostic to the Player implementation.
  • media-data: implementation of the domain module (media) using Media3.
  • media3-backend: Player on top of Media3 including functionalities such as avoiding playing music on the watch speaker.
  • media-sample: sample app to listen to downloaded music.
"},{"location":"media-toolkit/#architecture-overview","title":"Architecture overview","text":"

The Media Toolkit libraries are separated by layers (UI, domain and data) following the recommended app architecture .

The reason for including a domain layer is to provide flexibility to projects to use the UI library or the data library independently.

For example, if your project already contains an implementation for the player and you are only interested in using the media screens provided by the toolkit, then only the UI library needs to be added as a dependency. Thus, no extra dependencies ( e.g. Media3) will be added to your project.

On the other hand, if your project does not need any of the media screens or media UI components provided by the UI library, and you are only interested in the player implementation, then only the data library needs to be added as a dependency to your project.

"},{"location":"media-toolkit/#getting-started","title":"Getting started","text":"

The usage of the toolkit will vary according to the requirements of your project.

As per architecture overview, your project might not need to add all the libraries of the toolkit as dependency. If that\u2019s the case, refer to the documentation of each library required to your project for a guide on how to get started.

For a walkthrough on how to build a very simple media application using some libraries of the toolkit, refer to this guide.

For good reference on how to use all the libraries available in the toolkit, refer to the code of the media-sample app.

"},{"location":"media-ui/","title":"Media UI library","text":"

This library contains a set of composables for media player apps:

Individual controls like Play, Pause and Seek buttons; Components that might combine multiple controls, like PlayPauseButton and MediaControlButtons; Screens, like PlayerScreen, BrowseScreen and EntityScreen.

The previews of the composables can be found in the debug folder of the module source code.

This library is not dependent on any specific player implementation as per architecture overview.

"},{"location":"media-ui/#stateful-components","title":"Stateful components","text":"

Most of the components available in this library contain an overloaded version of themselves which accept either a UI model (MediaUiModel, PlaylistUiModel) or PlayerUiState or PlayerViewModel as parameters. We call those versions \u201cstateful components\u201d, which is a different definition from the compose documentation .

While the stateless components provide full customization, the stateful components provide convenience (if the default implementation suits your project requirements), as can be seen in the example below.

Stateless PodcastControlButtons usage:

PodcastControlButtons(\n    onPlayButtonClick = { },\n    onPauseButtonClick = { },\n    playPauseButtonEnabled = true,\n    playing = false,\n    percent = 0f,\n    onSeekBackButtonClick = { },\n    seekBackButtonEnabled = true,\n    onSeekForwardButtonClick = { },\n    seekForwardButtonEnabled = true,\n)\n

Stateful PodcastControlButtons usage:

PodcastControlButtons(\n    playerViewModel = viewModel,\n    playerUiState = playerUiState,\n)\n

Further examples on how to use these components can be found in the Stateful PlayerScreen guide.

"},{"location":"media/","title":"Media Domain library","text":"

This library currently contains a set of models and repositories that are common to media apps. But it can be expanded in the future to also include common use cases, as per domain layer guide.

The data and domain layer guides seem to imply that the definitions of the repositories should belong to the data layer. This would make the domain library dependent on a specific data library. In this project, the repositories are defined ( not implemented) in the domain layer. This makes the domain layer independent of external layers, and any implementation of the repositories can be used: the ones provided by the toolkit, or custom implementations provided by your project.

The reason for having a domain library is described in the architecture overview.

"},{"location":"media3-backend/","title":"Wear Media3 Backend library","text":""},{"location":"media3-backend/#features","title":"Features","text":"

The media3-backend module implements many of the suggested approaches at https://developer.android.com/training/wearables/overlays/audio. These enforce the best practices that will make you app work well for users on a range of Wear OS devices.

These build on top of the Media3 player featuring ExoPlayer which is optimised for Wear Playback, and is the standard playback engine for Wear OS media apps.

All functionality is demonstrated in the media-sample app, and as such is not described here with extensive code samples.

"},{"location":"media3-backend/#bluetooth-headphone-connections","title":"Bluetooth Headphone Connections","text":"

Any extended playback such as music, podcasts, or radio should only use a connected bluetooth speaker. See https://developer.android.com/training/wearables/principles#bluetooh-headphones for principles applying to Media and Wear OS apps generally.

When not connected, the default Watch Speaker will be used and so this must be actively avoided.

Horologist provides the PlaybackRules abstraction that allows you to intercept playback requests through your UI, a system media Tile, pressing a bluetooth headphone, or other services such as assistant.

public object Normal : PlaybackRules {\n    /**\n     * Can the given item be played with it's given state.\n     */\n    override suspend fun canPlayItem(mediaItem: MediaItem): Boolean = true\n\n    /**\n     * Can Media be played with the given audio target.\n     */\n    override fun canPlayWithOutput(audioOutput: AudioOutput): Boolean =\n        audioOutput is AudioOutput.BluetoothHeadset\n}\n

The WearConfiguredPlayer wraps the ExoPlayer to avoid starting playback and also pause immediately if the headset becomes disconnected. It will prompt the user to connect a headset at this point.

The AudioOutputSelector and default implementation BluetoothSettingsOutputSelector are used to prompt the user to connect a Bluetooth headset and then continue playback once connected.

public interface AudioOutputSelector {\n    /**\n     * Change from the current audio output, according to some sensible logic,\n     * and return when either the user has selected a new audio output or returning null\n     * if timed out.\n     */\n    public suspend fun selectNewOutput(currentAudioOutput: AudioOutput): AudioOutput?\n}\n
"},{"location":"media3-backend/#audio-offload","title":"Audio Offload","text":"

In line with https://exoplayer.dev/battery-consumption.html#audio-playback, Audio Offload allows your app to playback audio while in the background without waking up. This dramatically improves the users battery life, as well as decreasing the occurrences of Audio Underruns.

The AudioOffloadManager configures and controls Audio Offload, enabling sleeping while your app is in the background and disabling while in the foreground.

"},{"location":"media3-backend/#logging","title":"Logging","text":"

The media3-backend module interacts with ExoPlayer instance, but many events may be required for error handling, logging or metrics. Your can register your own Player.Listener with the ExoPlayer instance, but to receive generally useful events you can implement ErrorReporter to receive events and report with Android Log or write to a database.

Other things in the Horologist media libs will report events, and they all consistently use ErrorReporter to allow you to understand all activity in your app.

public interface ErrorReporter {\n    public fun logMessage(\n        message: String,\n        category: Category = Category.Unknown,\n        level: Level = Level.Info\n    )\n}\n
"},{"location":"media3-backend/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-media3-backend:<version>\"\n}\n
"},{"location":"network-awareness/","title":"Network Awareness library","text":""},{"location":"network-awareness/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-network-awareness:<version>\"\n}\n
"},{"location":"network-awareness/#problem-statement","title":"Problem Statement","text":"

On Wear choice of network is critical to efficient applications. See https://developer.android.com/training/wearables/data/network-access for more information.

The default behaviour is roughly

  • Use Paired Bluetooth connection when it is available.
  • Use Wifi when available and Charging.
  • Use LTE if no other network is available.
  • Wifi and Cell may be requested specifically by the application.

This leads to some suboptimal decisions

  • Downloading large downloads over the slow and shared bluetooth connection. This may overload the connection and starve other applications. Instead, Wifi or Cellular would be better used to quickly download and then close the connection.
  • Using LTE for trivial requests when not really required.
  • Using a single current network for all traffic regardless of length or important of the request.
"},{"location":"network-awareness/#functionality","title":"Functionality","text":"

This library allows defining rules based on the RequestType and NetworkType, currently integrated into OkHttp.

public interface NetworkingRules {\n    /**\n     * Is this request considered high bandwidth and should activate LTE or Wifi.\n     */\n    public fun isHighBandwidthRequest(requestType: RequestType): Boolean\n\n    /**\n     * Checks whether this request is allowed on the current network type.\n     */\n    public fun checkValidRequest(\n        requestType: RequestType,\n        currentNetworkInfo: NetworkInfo\n    ): RequestCheck\n\n    /**\n     * Returns the preferred network for a request.\n     *\n     * Null means no suitable network.\n     */\n    public fun getPreferredNetwork(\n        networks: Networks,\n        requestType: RequestType\n    ): NetworkStatus?\n}\n

It allow allows logging network usage and visibility into network status.

See media-sample for an example. Key classes to observe usage of

  • UampNetworkingRules - defining the app specific rules for the network.
  • MediaInfoTimeText - An example of displaying network status and usage to the user.
  • NetworkAwareCallFactory - Used to wrap an OkHttp Call.Factory when passed to a library such as Coil.
  • NetworkSelectingCallFactory - Used to apply the networking rules to OkHttp.
  • NetworkRepository - The Repository for the current network state.
  • HighBandwidthNetworkMediator - a mediator for requesting high bandwidth networks.
"},{"location":"simple-media-app-guide/","title":"Build a simple media player app","text":"

This guide will walk you through on how to build a very simple media player app for Wear OS, capable of playing a media which is hosted on the internet.

This guide assumes that you are familiar with:

  • How to create Wear OS projects in Android Studio;
  • Kotlin programming language;
  • Jetpack Compose;
"},{"location":"simple-media-app-guide/#display-a-playerscreen","title":"Display a PlayerScreen","text":""},{"location":"simple-media-app-guide/#1-add-dependency","title":"1 - Add dependency","text":"

Create a new project from Android Studio by choosing \"Basic Wear App Without Associated Tiles\" from \"Wear OS\" templates. Add dependency on media-ui to your project\u2019s build.gradle:

implementation \"com.google.android.horologist:horologist-media-ui:$horologist_version\"\n
"},{"location":"simple-media-app-guide/#2-add-playerscreen","title":"2 - Add PlayerScreen","text":"

Add the following code to your Activity\u2019s onCreate function:

setContent {\n    PlayerScreen(\n        mediaDisplay = {\n            TextMediaDisplay(\n                title = \"Song name\",\n                subtitle = \"Artist name\"\n            )\n        },\n        controlButtons = {\n            PodcastControlButtons(\n                onPlayButtonClick = { },\n                onPauseButtonClick = { },\n                playPauseButtonEnabled = true,\n                playing = false,\n                onSeekBackButtonClick = { },\n                seekBackButtonEnabled = true,\n                onSeekForwardButtonClick = { },\n                seekForwardButtonEnabled = true,\n            )\n        },\n        buttons = { }\n    )\n}\n

This code is displaying PlayerScreen on the app. PlayerScreen is a full screen composable that contains slots parameters to pass the contents to be displayed for media display, control buttons and more.

In this sample, we are using the UI components TextMediaDisplay and PodcastControlButtons, provided by the UI library, as values to parameters of PlayerScreen.

"},{"location":"simple-media-app-guide/#result","title":"Result","text":"

Run the app and you should see the following screen:

None of the controls are working, as they were not implemented yet.

"},{"location":"simple-media-app-guide/#make-the-screen-functional","title":"Make the screen functional","text":""},{"location":"simple-media-app-guide/#1-add-dependencies","title":"1 - Add dependencies","text":"

Add the following dependencies to your project\u2019s build.gradle:

implementation \"com.google.android.horologist:horologist-media-data:$horologist_version\"\nimplementation \"com.google.android.horologist:horologist-audio-ui:$horologist_version\"\nimplementation(\"androidx.media3:media3-exoplayer:$media3_version\")\n
"},{"location":"simple-media-app-guide/#2-add-viewmodel","title":"2 - Add ViewModel","text":"

Add a ViewModel extending PlayerViewModel, providing an instance of PlayerRepositoryImpl:

class MyViewModel(\n    player: Player,\n    playerRepository: PlayerRepositoryImpl = PlayerRepositoryImpl()\n) : PlayerViewModel(playerRepository) {}\n
"},{"location":"simple-media-app-guide/#3-add-init-block","title":"3 - Add init block","text":"

Add the following init block to the ViewModel to connect the Player to the PlayerRepository, set a media and update the position of the player every second:

init {\n    viewModelScope.launch {\n        playerRepository.connect(player) {}\n\n        playerRepository.setMedia(\n            Media(\n                id = \"wake_up_02\",\n                uri = \"https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/02_-_Geisha.mp3\",\n                title = \"Geisha\",\n                artist = \"The Kyoto Connection\"\n            )\n        )\n    }\n}\n
"},{"location":"simple-media-app-guide/#4-create-an-instance-of-the-viewmodel","title":"4 - Create an instance of the ViewModel","text":"

Change your Activity\u2019s onCreate function to:

@SuppressLint(\"UnsafeOptInUsageError\")\nval player = ExoPlayer.Builder(this)\n    .setSeekForwardIncrementMs(5000L)\n    .setSeekBackIncrementMs(5000L)\n    .build()\n// ViewModels should NOT be created here like this\nval viewModel = MyViewModel(player)\nval volumeViewModel = createVolumeViewModel()\n\nsetContent {\n    PlayerScreen(\n        playerViewModel = viewModel,\n        volumeViewModel = volumeViewModel,\n        mediaDisplay = { playerUiState: PlayerUiState ->\n          DefaultMediaInfoDisplay(playerUiState)\n        },\n        controlButtons = { playerUIController: PlayerUiController,\n                           playerUiState: PlayerUiState ->\n          PodcastControlButtons(\n                  playerController = playerUIController,\n                  playerUiState = playerUiState\n          )\n        },\n        buttons = { }\n    )\n}\n

Add createVolumeViewModel function to create a VolumeViewModel:

fun createVolumeViewModel(): VolumeViewModel {\n    val audioRepository = SystemAudioRepository.fromContext(application)\n    val vibrator: Vibrator = application.getSystemService(Vibrator::class.java)\n    return VolumeViewModel(audioRepository, audioRepository, onCleared = {\n        audioRepository.close()\n    }, vibrator)\n}\n

We are creating an instance of ExoPlayer, passing it to the ViewModel.

Then for the PlayerScreen slots we are using:

  • the DefaultMediaDisplay component, which accepts a MediaUiModel instance as parameter;
  • the stateful version of PodcastControlButtons, which accepts instances of PlayerViewModel and PlayerUiStateas parameters to hook the controls with the ViewModel;
"},{"location":"simple-media-app-guide/#result_1","title":"Result","text":"

Run the app again and this time, play with the screen controls as the app should be able to play, pause, and seek the media now:

"},{"location":"tiles/","title":"Tiles Library","text":""},{"location":"tiles/#suspendingtileservice","title":"SuspendingTileService","text":"

Provides a SuspendingTileService, which also acts as a LifecycleService.

class ExampleTileService : SuspendingTileService() {\n    override suspend fun tileRequest(requestParams: RequestBuilders.TileRequest): Tile {\n        return Tile.Builder()\n            // create your tile here\n            .build()\n    }\n\n    override suspend fun resourcesRequest(\n        requestParams: RequestBuilders.ResourcesRequest\n    ): ResourceBuilders.Resources = ResourceBuilders.Resources.Builder().setVersion(\"1\").build()\n}\n
"},{"location":"tiles/#coil-image-helpers","title":"Coil Image Helpers","text":"

Provides a suspending method to load an image from the network, convert to an RGB_565 bitmap, and encode as a Tiles InlineImageResource.

val imageResource = imageLoader.loadImageResource(applicationContext, \n    \"https://media.githubusercontent.com/media/google/horologist/main/docs/media-ui/playerscreen.png\") {\n    // Show a local error image if missing\n    error(R.drawable.missingImage)\n}\n
"},{"location":"tiles/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-tiles:<version>\"\n}\n
"},{"location":"updating-old/","title":"Updating & releasing Horologist","text":"

This guide is currently not in use. See updating.md instead.

This doc is mostly for maintainers.

"},{"location":"updating-old/#new-features-bugfixes","title":"New features & bugfixes","text":"

All new features should be uploaded as PRs against the main branch.

Once merged into main, they will be automatically merged into the snapshot branch.

"},{"location":"updating-old/#jetpack-compose-snapshots","title":"Jetpack Compose Snapshots","text":"

We publish snapshot versions of Horologist, which depend on a SNAPSHOT versions of Jetpack Compose. These are built from the snapshot branch.

"},{"location":"updating-old/#updating-to-a-newer-compose-snapshot","title":"Updating to a newer Compose snapshot","text":"

As mentioned above, updating to a new Compose snapshot is done by submitting a new PR against the snapshot branch:

git checkout snapshot && git pull\n# Create branch for PR\ngit checkout -b update_snapshot\n

Now edit the project to depend on the new Compose SNAPSHOT version:

Edit /gradle/libs.versions.toml:

Under [versions]:

  1. Update the composesnapshot property to be the snapshot number
  2. Ensure that the compose property is correct

Make sure the project builds and test pass:

./gradlew check\n

Now git commit the changes and push to GitHub.

Finally create a PR (with the base branch as snapshot) and send for review.

"},{"location":"updating-old/#releasing","title":"Releasing","text":"

Once the next Jetpack Compose version is out, we're ready to push a new release:

"},{"location":"updating-old/#1-merge-snapshot-into-main","title":"#1: Merge snapshot into main","text":"

First we merge the snapshot branch into main:

git checkout snapshot && git pull\ngit checkout main && git pull\n\n# Create branch for PR\ngit checkout -b main_snapshot_merge\n\n# Merge in the snapshot branch\ngit merge snapshot\n
"},{"location":"updating-old/#2-update-dependencies","title":"#2: Update dependencies","text":"

Edit /gradle/libs.versions.toml:

Under [versions]:

  1. Update the composesnapshot property to a single character (usually -). This disables the snapshot repository.
  2. Update the compose property to match the new release (i.e. 1.0.0-beta06)

Make sure the project builds and test pass:

./gradlew check\n

Commit the changes.

"},{"location":"updating-old/#3-bump-the-version-number","title":"#3: Bump the version number","text":"

Edit gradle.properties:

  • Update the VERSION_NAME property and remove the -SNAPSHOT suffix.

Commit the changes, using the commit message containing the new version name.

"},{"location":"updating-old/#4-push-to-github","title":"#4: Push to GitHub","text":"

Push the branch to GitHub and create a PR against the main branch, and send for review. Once approved and merged, it will be automatically deployed to Maven Central.

"},{"location":"updating-old/#5-create-release","title":"#5: Create release","text":"

Once the above PR has been approved and merged, we need to create the GitHub release:

  • Open up the Releases page.
  • At the top you should see a 'Draft' release, auto populated with any PRs since the last release. Click 'Edit'.
  • Make sure that the version number matches what we released (the tool guesses but is not always correct).
  • Double check everything, then press 'Publish release'.

At this point the release is published. This will trigger the docs action to run, which will auto-deploy a new version of the website.

"},{"location":"updating-old/#6-prepare-the-next-development-version","title":"#6: Prepare the next development version","text":"

The current release is now finished, but we need to update the version for the next development version:

Edit gradle.properties:

  • Update the VERSION_NAME property, by increasing the version number, and adding the -SNAPSHOT suffix.
  • Example: released version: 0.3.0. Update to 0.3.1-SNAPSHOT

git commit and push to main.

Finally, merge all of these changes back to snapshot:

git checkout snapshot && git pull\ngit merge main\ngit push\n
"},{"location":"updating/","title":"Updating & releasing Horologist","text":"

This doc is mostly for maintainers.

Ensure your Sonatype JIRA credentials are set in your environment variables.

export ORG_GRADLE_PROJECT_mavenCentralUsername=username\nexport ORG_GRADLE_PROJECT_mavenCentralPassword=password\n

Decrypt the signing key to release a public build.

release/signing-setup.sh '<Horologist AES key>'\ngradlew clean publish --no-parallel --stacktrace\nrelease/signing-cleanup.sh\n

The deployment then needs to be manually released via the Nexus Repository Manager. See Releasing Deployment from OSSRH.

"},{"location":"updating/#snapshot-release","title":"Snapshot release","text":"

For a snapshot release, the signing key is not used. Ensure VERSION_NAME in gradle.properties has the -SNAPSHOT suffix or specify the version via -PVERSION_NAME=....

gradlew -PVERSION_NAME=0.0.1-SNAPSHOT clean publish --no-parallel --stacktrace\n
"},{"location":"using-snapshot-version/","title":"Using a Snapshot Version of the Library","text":"

If you would like to depend on the cutting edge version of the Horologist library, you can use the snapshot versions that are published to Sonatype OSSRH's snapshot repository. These are updated on every commit to main.

To do so:

repositories {\n    // ...\n    maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }\n}\n\ndependencies {\n    // Check the latest SNAPSHOT version from the link above\n    classpath 'com.google.android.horologist:horologist-tiles:XXX-SNAPSHOT'\n}\n

You might see a number of different versioned snapshots. If we use an example:

  • 0.3.0-SNAPSHOT is a build from the main branch, and depends on the latest tagged Jetpack Compose release (i.e. alpha03).
  • 0.3.0.compose-6574163-SNAPSHOT is a build from the snapshot branch. This depends on the SNAPSHOT build of Jetpack Compose from build 6574163. You should only use these if you are using Jetpack Compose snapshot versions (see below).
"},{"location":"using-snapshot-version/#using-jetpack-compose-snapshots","title":"Using Jetpack Compose Snapshots","text":"

If you're using SNAPSHOT versions of the androidx.compose libraries, you might run into issues with the current stable Horologist release forcing an older version of those libraries.

We publish snapshot versions of Horologist which depend on recent Jetpack Compose SNAPSHOT repositories. To find a recent build, look through the snapshot repository for any versions in the scheme x.x.x.compose-YYYY-SNAPSHOT (for example: 0.3.0.compose-6574163-SNAPSHOT). The YYYY in the scheme is the snapshot build being used from AndroidX (from the example: build 6574163). You can then use it like so:

repositories {\n    // ...\n    maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }\n}\n\ndependencies {\n    // Check the latest SNAPSHOT version from the link above\n    classpath 'com.google.android.horologist:horologist-tiles:XXXX.compose-YYYYY-SNAPSHOT'\n}\n

These builds are updated regularly, but there's no guarantee that we will create one for a given snapshot number.

Note: you might also see versions in the scheme x.x.x.ui-YYYY-SNAPSHOT. These are the same, just using an older suffix.

"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Overview","text":"

Horologist is a group of libraries that aim to supplement Wear OS developers with features that are commonly required by developers but not yet available.

"},{"location":"#maintained-versions","title":"Maintained Versions","text":"

The currently maintained branches of Horologist are.

Version Branch Description 0.4.x release-0.4.x Wear Compose 1.2.x (stable) and Media3, and generally stable APIs. 0.5.x main Wear Compose 1.3 alpha, Media3 and generally latest alphas of Androidx."},{"location":"#media","title":"\ud83c\udfb5 Media","text":"

Horologist provides the Media Toolkit: a set of libraries to build Media apps on Wear OS and a sample app that you can run to see the toolkit in action.

The toolkit includes:

  • horologist-media-ui: common media UI components and screens like PlayerScreen.
  • horologist-media: domain model for Media related functionality. Provides an abstraction to the UI module (horologist-media-ui) that is agnostic to the Player implementation.
  • horologist-media-data: implementation of the domain module (horologist-media) using Media3.
  • horologist-media3-backend: Player on top of Media3 including functionalities such as avoiding playing music on the watch speaker.
  • media sample: sample app to listen to downloaded music.
Player Screen Browse Screen Entity Screen"},{"location":"#composables","title":"\ud83d\udcc5 Composables","text":"

High quality prebuilt composables, such as Time and Date pickers.

  • horologist-composables
DatePicker TimePickerWith12HourClock TimePicker SegmentedProgressIndicator SquareSegmentedProgressIndicator"},{"location":"#compose-layout","title":"\ud83d\udcd0 Compose Layout","text":"

Layout related functionality such as a Navigation Aware Scaffold.

  • horologist-compose-layout
fillMaxRectangle()"},{"location":"#compose-material","title":"\ud83d\udd32 Compose Material","text":"

Opinionated implementation of the components of the Wear Material Compose library , based on the specifications of Wear Material Design Kit .

  • horologist-compose-material
"},{"location":"#audio-and-ui","title":"\ud83d\udd0a Audio and UI","text":"

Domain model for Audio related functionality. Volume Control, Output switching. Subscribing to a Flow of changes in audio or output.

  • horologist-audio
  • horologist-audio-ui
VolumeScreen"},{"location":"#auth","title":"\ud83d\udd10 Auth","text":"

Libraries to help developers to build apps following the Sign-In guidelines for Wear OS .

  • horologist-auth-composables: composable screens for authentication use cases, with no dependency on the auth-data library.
  • horologist-auth-ui: composable screens for authentication use cases, with integration with the auth-data library
  • horologist-auth-data: implementation for Wear apps for most of the authentication methods listed in the Authentication on wearables guide.
  • horologist-auth-data-phone: implementation for Mobile apps for some of the authentication methods provided by the auth-data library.
  • sample wear: sample wear app to authenticate using different methods.
  • sample phone: sample phone app to authenticate using different methods.
"},{"location":"#tiles","title":"\u2630 Tiles","text":"

Kotlin coroutines flavoured TileService.

horologist-tiles

"},{"location":"#why-the-name","title":"Why the name?","text":"

The name mirrors the Accompanist name, and is also Watch related.

https://en.wiktionary.org/wiki/horologist

horologist (Noun) Someone who makes or repairs timepieces, watches or clocks.

"},{"location":"#contributions","title":"Contributions","text":"

Please contribute! We will gladly review any pull requests submitted. Make sure to read the Contributing page to know what our expectations of contributions are.

"},{"location":"#license","title":"License","text":"
Copyright 2023 The Android Open Source Project\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n
"},{"location":"audio-ui/","title":"Audio Settings UI library","text":""},{"location":"audio-ui/#volume-screen","title":"Volume Screen","text":"

A volume screen, showing the current audio output (headphones, speakers) and allowing to change the button with a stepper or bezel.

VolumeScreen(focusRequester = focusRequester)\n

"},{"location":"audio-ui/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-audio-ui:<version>\"\n}\n
"},{"location":"audio/","title":"Audio Settings Library","text":"

Domain model for Volume and Audio Output.

val audioRepository = SystemAudioRepository.fromContext(application)\n\naudioRepository.increaseVolume()\n\nval volumeState: StateFlow<VolumeState> = audioRepository.volumeState\n\nval audioOutput: StateFlow<AudioOutput> = audioRepository.audioOutput\n\nval output = audioOutput.value\nif (output is AudioOutput.BluetoothHeadset) {\n  println(output.name)\n}\n
"},{"location":"audio/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-audio:<version>\"\n}\n
"},{"location":"auth-composables/","title":"Auth Composables","text":"

This library contains a set of composables screens and components related to authentication.

The previews of the composables can be found in the debug folder of the module source code.

This library is not dependent on any specific authentication implementation as per architecture overview.

"},{"location":"auth-composables/#screens","title":"Screens","text":"
  • SignInPlaceholderScreen

  • SelectAccountScreen

  • CheckYourPhoneScreen

"},{"location":"auth-composables/#dialogs","title":"Dialogs","text":"
  • SignedInConfirmationDialog
"},{"location":"auth-composables/#chips","title":"Chips","text":"
  • CreateAccountChip

  • GuestModeChip

  • OtherOptionsChip

  • SignInChip

"},{"location":"auth-data-phone/","title":"Auth Data Phone","text":"

This library contains implementation for Mobile apps for some of the authentication methods provided by the auth-data library.

"},{"location":"auth-data-phone/#token-sharing","title":"Token sharing","text":"
  • TokenBundleRepository
    • TokenBundleRepositoryImpl
"},{"location":"auth-data/","title":"Auth Data","text":"

This library contains implementation for Wear apps for most of the authentication methods listed in the Authentication on wearables guide.

The repositories of this library are built mainly to support the components from auth-ui library, but can be used with your own UI components.

"},{"location":"auth-data/#token-sharing","title":"Token sharing","text":"
  • TokenBundleRepository
    • TokenBundleRepositoryImpl
"},{"location":"auth-data/#google-sign-in","title":"Google Sign-In","text":"
  • GoogleSignInAuthUserRepository
"},{"location":"auth-data/#oauth-pkce","title":"OAuth (PKCE)","text":"
  • PKCEConfigRepository
    • PKCEConfigRepositoryGoogleImpl
  • PKCEOAuthCodeRepository
    • PKCEOAuthCodeRepositoryImpl
  • PKCETokenRepository
    • PKCETokenRepositoryGoogleImpl
"},{"location":"auth-data/#oauth-device-grant","title":"OAuth (Device Grant)","text":"
  • DeviceGrantConfigRepository
    • DeviceGrantConfigRepositoryDefaultImpl
  • DeviceGrantTokenRepository
    • DeviceGrantTokenRepositoryGoogleImpl
  • DeviceGrantVerificationInfoRepository
    • DeviceGrantVerificationInfoRepositoryGoogleImpl
"},{"location":"auth-googlesignin-guide/","title":"Google Sign-In guide","text":"

This guide will walk you through on how to display a screen on your watch app so that users can select their Google account to sign-in to your app.

"},{"location":"auth-googlesignin-guide/#requirements","title":"Requirements","text":"

Follow the setup instructions for integrating Google Sign-in into an Android app from this link.

"},{"location":"auth-googlesignin-guide/#getting-started","title":"Getting started","text":"
  1. Add dependencies

    Add the following dependencies to your project\u2019s build.gradle:

    dependencies {\n    implementation \"com.google.android.horologist:horologist-auth-composables:<version>\"\n    implementation \"com.google.android.horologist:horologist-auth-ui:<version>\"\n    implementation \"com.google.android.horologist:horologist-compose-material:<version>\"\n}\n
  2. Create an instance of GoogleSignInClient

    Create an instance of GoogleSignInClient, according to your requirements, for example:

    val googleSignInClient = GoogleSignIn.getClient(\n    applicationContext,\n    GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)\n        .requestEmail()\n        .build()\n)\n
"},{"location":"auth-googlesignin-guide/#display-the-screen","title":"Display the screen","text":"
  1. Create a ViewModel

    Create your implementation of GoogleSignInViewModel, passing the GoogleSignInClient created:

    class MyGoogleSignInViewModel(\n    googleSignInClient: GoogleSignInClient,\n) : GoogleSignInViewModel(googleSignInClient)\n
  2. Display the screen

    Display the GoogleSignInScreen passing an instance of the GoogleSignInViewModel created:

    GoogleSignInScreen(\n   onAuthCancelled = { /* code to navigate to another screen on this event */ },\n   onAuthSucceed = { /* code to navigate to another screen on this event */ },\n   viewModel = hiltViewModel<MyGoogleSignInViewModel>()\n)\n

This sample uses Hilt to retrieve an instance of the ViewModel, but you should use what suits your project best, see this link for more info.

"},{"location":"auth-googlesignin-guide/#retrieve-the-signed-in-account","title":"Retrieve the signed in account","text":"

In order to have access an instance of the GoogleSignInAccount selected by the user, follow the steps:

  1. Implement GoogleSignInEventListener

    class GoogleSignInEventListenerImpl : GoogleSignInEventListener {\n    override suspend fun onSignedIn(account: GoogleSignInAccount) {\n        // your implementation using the account parameter\n    }\n}\n
  2. Pass the listener to the ViewModel

    Pass an instance of GoogleSignInEventListener to GoogleSignInViewModel:

    class MyGoogleSignInViewModel(\n googleSignInClient: GoogleSignInClient,\n googleSignInEventListener: GoogleSignInEventListener,\n) : GoogleSignInViewModel(googleSignInClient, googleSignInEventListener)\n
"},{"location":"auth-overview/","title":"Auth libraries","text":""},{"location":"auth-overview/#overview","title":"Overview","text":"

The purpose of the auth libraries is to:

  • help developers to build apps following the Sign-In guidelines for Wear OS;
  • provide implementation (for Wear and Mobile) for most of the authentication methods listed in the Authentication on wearables guide;

The following libraries are provided:

  • auth-composables: composable screens for Authentication use cases, with no dependency on the auth-data library.
  • auth-ui: composable screens for Authentication use cases, with integration with the auth-data library.
  • auth-data: implementation for Wear apps for most of the authentication methods listed in the Authentication on wearables guide.
  • auth-data-phone: implementation for Mobile apps for some of the authentication methods provided by the auth-data library.

The following sample apps are also provided:

  • auth-sample-wear: sample wear app to authenticate using different methods.
  • auth-sample-phone: sample phone app to authenticate using different methods.
"},{"location":"auth-overview/#architecture-overview","title":"Architecture overview","text":"

The auth libraries are separated by layers (UI and data), following the recommended app architecture. The reason for including an extra UI library (auth-composables) is to provide flexibility to projects that would like to only use the UI components that are not dependent on auth-data.

"},{"location":"auth-overview/#getting-started","title":"Getting started","text":"

The usage of the auth libraries will vary according to the requirements of your project.

As per architecture overview, your project might not need to add all the auth libraries as dependency. If that\u2019s the case, refer to the documentation of each library required to your project for a guide on how to get started.

"},{"location":"auth-sample-apps/","title":"Auth sample apps","text":""},{"location":"auth-sample-apps/#wear-sample","title":"Wear sample","text":"

The app showcases the implementation of the following authentication methods:

  • Token sharing
  • OAuth (PKCE)
  • OAuth (Device Grant)
  • Google Sign-In
"},{"location":"auth-sample-apps/#phone-sample","title":"Phone sample","text":"

The app showcases the implementation of the following authentication methods:

  • Token sharing
"},{"location":"auth-tokenshare-guide/","title":"Token sharing guide","text":"

This guide will walk you through on how to securely transfer authentication data from the phone app to the watch app using Horologist's Auth libraries.

"},{"location":"auth-tokenshare-guide/#requirements","title":"Requirements","text":"

Horologist Auth library is built on top of Wearable Data Layer API, so your phone and watch apps must:

  • have APK signatures and the signature schemes identical;
  • the same package name;
"},{"location":"auth-tokenshare-guide/#getting-started","title":"Getting started","text":"
  1. Add dependencies

    Add the following dependencies to your project\u2019s build.gradle.

    For the phone app project:

    dependencies {\n    implementation \"com.google.android.horologist:horologist-auth-data-phone:<version>\"\n    implementation \"com.google.android.horologist:horologist-datalayer:<version>\"\n}\n

    For the watch app project:

    dependencies {\n    implementation \"com.google.android.horologist:horologist-auth-data:<version>\"\n    implementation \"com.google.android.horologist:horologist-datalayer:<version>\"\n}\n
  2. Add capability to phone app project

    On the phone app project, add a wear.xml file in the res/values folder with the following content:

    <resources>\n    <string-array name=\"android_wear_capabilities\">\n        <item>horologist_phone</item>\n    </string-array>\n</resources>\n
  3. Create a WearDataLayerRegistry

    In both projects, create an instance of WearDataLayerRegistry from the datalayer:

    val registry = WearDataLayerRegistry.fromContext(\n    application = // application context,\n    coroutineScope = // a coroutine scope\n)\n

    This class should be created as a singleton in your app.

  4. Define the data to be transferred

    Define which authentication data that should be transferred from the phone to the watch. It can be a data class with many properties, it can also be a protocol buffer. For this guide, we will pass a simple String instance.

  5. Create a Serializer for the data

    Create a DataStore Serializer class for the class defined to be transferred from the phone to the watch (String for this guide):

    public object TokenSerializer : Serializer<String> {\n    override val defaultValue: String = \"\"\n\n    override suspend fun readFrom(input: InputStream): String =\n        InputStreamReader(input).readText()\n\n    override suspend fun writeTo(t: String, output: OutputStream) {\n        withContext(Dispatchers.IO) {\n            output.write(t.toByteArray())\n        }\n    }\n}   \n

    This class should preferable be placed in a shared module between the phone and watch projects, but could also be duplicated in both projects.

    More information about this serialization in this blog post.

"},{"location":"auth-tokenshare-guide/#send-authentication-data-from-the-phone","title":"Send authentication data from the phone","text":"
  1. Create a TokenBundleRepository on the phone project

    Create an instance of TokenBundleRepository on the phone app project:

    val tokenBundleRepository = TokenBundleRepositoryImpl(\n    registry = registry,\n    coroutineScope = // a coroutine scope,\n    serializer = TokenSerializer\n)   \n
  2. Check if the repository is available (optional)

    Before using the repository, you can check if it is available to be used on the current device with:

    tokenBundleRepository.isAvailable()\n

    If the repository is not available on the device, all the calls to it will fail silently.

    See the requirements of Wearable Data Layer API.

  3. Send authentication data

    The authentication data can be sent from the phone calling update:

    tokenBundleRepository.update(\"token\")\n
"},{"location":"auth-tokenshare-guide/#receive-authentication-data-on-the-watch","title":"Receive authentication data on the watch","text":"
  1. Create a TokenBundleRepository on the watch project

    Create an instance of TokenBundleRepository on the watch app project:

    val tokenBundleRepository = TokenBundleRepositoryImpl.create(\n    registry = registry,\n    serializer = TokenSerializer\n)\n
  2. Receive authentication data

    The authentication data can be listened from the watch via the flow property:

    tokenBundleRepository.flow\n

"},{"location":"auth-ui/","title":"Auth UI","text":"

This library contains a set of composables screens and components related to authentication.

The previews of the composables can be found in the debug folder of the module source code.

The composables of this module might depend on repository interfaces defined in auth-data library. The implementation of these repositories does not necessarily need to be from auth-data, they can be your own implementation. Some of the composables might depend on an external library.

"},{"location":"auth-ui/#screens","title":"Screens","text":""},{"location":"auth-ui/#common","title":"Common","text":""},{"location":"auth-ui/#signinpromptscreen","title":"SignInPromptScreen","text":"

A screen to prompt users to sign in.

It helps achieve to the following best practices:

  • Explain sign-in benefits
  • Provide alternatives
"},{"location":"auth-ui/#google-sign-in","title":"Google Sign-In","text":""},{"location":"auth-ui/#googlesigninscreen","title":"GoogleSignInScreen","text":"

A screen for the Google Sign-In authentication method.

It uses different screens from auth-composables to display the full authentication flow.

It relies on the Google Sign-In for Android library for authentication and account selection. So an instance of GoogleSignInClient has to be provided to GoogleSignInViewModel.

"},{"location":"auth-ui/#oauth","title":"OAuth","text":""},{"location":"auth-ui/#pkcesigninscreen","title":"PKCESignInScreen","text":"

A screen for the OAuth (PKCE) authentication method.

It uses different screens from auth-composables to display the full authentication flow.

A implementation for the following repositories are required to be provided:

  • PKCEConfigRepository
  • PKCEOAuthCodeRepository
  • PKCETokenRepository
"},{"location":"auth-ui/#devicegrantsigninscreen","title":"DeviceGrantSignInScreen","text":"

A screen for the OAuth (Device Grant) authentication method.

It uses different screens from auth-composables to display the full authentication flow.

A implementation for the following repositories are required to be provided:

  • DeviceGrantConfigRepository
  • DeviceGrantVerificationInfoRepository
  • DeviceGrantTokenRepository
"},{"location":"composables/","title":"Composables library","text":""},{"location":"composables/#date-picker","title":"Date Picker","text":""},{"location":"composables/#segmented-progress-indicator","title":"Segmented Progress Indicator","text":""},{"location":"composables/#time-pickers","title":"Time Pickers","text":""},{"location":"composables/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-composables:<version>\"\n}\n
"},{"location":"compose-layout/","title":"Compose Layout library","text":""},{"location":"compose-layout/#scalinglazycolumn-responsive-layout","title":"ScalingLazyColumn responsive() layout.","text":"

The responsive() layout factory will ensure that your ScalingLazyColumn is positioned correctly on all screen sizes.

Pass in a boolean for the firstItemIsFullWidth param to indicate whether the first item can fit just below TimeText, or must be shifted down further to avoid cutting off the edges.

The overloaded ScalingLazyColumn composable with ScalingLazyColumnState param, when combined with responsive() will handle all the following:

  • Position the first item near the top on all screen sizes.
  • Ensure the last item can be scrolled into view.
  • Handle RSB/Bezel scrolling with Fling.
  • Size side margins based on a percentage, adapting to different screen sizes.
val columnState =\n    rememberColumnState(ScalingLazyColumnDefaults.responsive(firstItemIsFullWidth = false))\n\nScaffold(\n    modifier = Modifier\n        .fillMaxSize(),\n    timeText = {\n        TimeText(modifier = Modifier.scrollAway(columnState))\n    }\n) {\n    ScalingLazyColumn(\n        modifier = Modifier.fillMaxSize(),\n        columnState = columnState,\n    ) {\n        item {\n            ListHeader {\n                Text(\n                    text = \"Main\",\n                    modifier = Modifier.fillMaxWidth(0.6f),\n                    textAlign = TextAlign.Center\n                )\n            }\n        }\n        items(10) {\n            Chip(\"Item $it\", onClick = {})\n        }\n    }\n}\n
"},{"location":"compose-layout/#navigation-scaffold","title":"Navigation Scaffold.","text":"

Syncs the TimeText, PositionIndicator and Scaffold to the current navigation destination state. The TimeText will scroll out of the way of content automatically.

WearNavScaffold(\n    startDestination = \"home\",\n    navController = navController\n) {\n    scalingLazyColumnComposable(\n        \"home\",\n        scrollStateBuilder = { ScalingLazyListState(initialCenterItemIndex = 0) }\n    ) {\n        MenuScreen(\n            scrollState = it.scrollableState,\n            focusRequester = it.viewModel.focusRequester\n        )\n    }\n\n    scalingLazyColumnComposable(\n        \"items\",\n        scrollStateBuilder = { ScalingLazyListState() }\n    ) {\n        ScalingLazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .scrollableColumn(it.viewModel.focusRequester, it.scrollableState),\n            state = it.scrollableState\n        ) {\n            items(100) {\n                Text(\"i = $it\")\n            }\n        }\n    }\n\n    scrollStateComposable(\n        \"settings\",\n        scrollStateBuilder = { ScrollState(0) }\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .verticalScroll(state = it.scrollableState)\n                .scrollableColumn(focusRequester = it.viewModel.focusRequester, scrollableState = it.scrollableState),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            (1..100).forEach {\n                Text(\"i = $it\")\n            }\n        }\n    }\n}\n
"},{"location":"compose-layout/#box-inset-layout","title":"Box Inset Layout.","text":"

Use as a break glass for simple layout to fit within a safe square.

Box(\n    modifier = Modifier\n        .fillMaxRectangle()\n) {\n    // App Content here        \n}\n

"},{"location":"compose-layout/#fade-away-modifier","title":"Fade Away Modifier","text":""},{"location":"compose-layout/#ambientaware-composable","title":"AmbientAware composable","text":"

AmbientAware allows your UI to react to ambient mode changes. For more information on how Ambient mode and Always-on work on Wear OS, see the developer guidance.

You should place this composable high up in your design, as it alters the behavior of the Activity.

@Composable\nfun WearApp() {\n    AmbientAware { ambientStateUpdate ->\n        // App Content here\n    }\n}\n

If you need some screens to use always-on, and others not to, then you can use the additional argument supplied to AmbientAware.

For example, in a workout app, it is desirable that the main workout screen uses always-on, but the workout summary at the end does not. See the ExerciseClient guide and samples for more information on building a workout app.

@Composable\nfun WearApp() {\n    // Hoist state here for your current screen logic\n\n    AmbientAware(isAlwaysOnScreen = currentScreen.useAlwaysOn) { ambientStateUpdate ->\n        // App Content here\n    }\n}\n
"},{"location":"compose-layout/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-compose-layout:<version>\"\n}\n
"},{"location":"compose-material/","title":"Compose Material","text":"

A library providing opinionated implementation of the components of the Wear Material Compose library, based on the specifications of Wear Material Design Kit.

"},{"location":"compose-material/#motivation","title":"Motivation","text":"

In order to display a Chip component in a Wear OS app, using Wear Material Compose library, the code would look like this:

Chip(\n    label = { Text(\"Primary label\") },\n    onClick = { },\n    secondaryLabel = { Text(\"Secondary label\") },\n    icon = { Icon(imageVector = Icons.Default.Image, contentDescription = null) }\n)\n

In comparison, using Horologist's Compose Material library, the code would look simpler:

Chip(\n    label = \"Primary label\",\n    onClick = { },\n    secondaryLabel = \"Secondary label\",\n    icon = Icons.Default.Image\n)\n

As seen above, Horologist's Compose Material provides convenient ways of passing parameters to the components. Furthermore, for this particular component, it will also take care of:

  • Define the maximum number of lines for each label, truncating them appropriately, even when the user has changed the font size in the OS settings;
  • Change the text alignment and maximum number of lines of the primary label when the secondary label is not present;
  • Adjust the content padding based on the icon size;

The list is not exhaustive and it varies for each individual component.

"},{"location":"compose-material/#when-this-library-should-not-be-used","title":"When this library should not be used?","text":"

If the specifications for the component needed in your app does not match the specifications of the components listed in Wear Material Design Kit, then Wear Material Compose library should be used instead.

"},{"location":"compose-tools/","title":"Compose Tools library","text":""},{"location":"compose-tools/#tile-previews","title":"Tile Previews.","text":"

Android Studio Preview support for tiles, using the TilesRenderer inside and AndroidView. Uses either raw Tiles proto, or the TilesLayoutRenderer abstraction to define a predictable process for generating a Tile for a given state.

@WearPreviewDevices\n@WearPreviewFontSizes\n@Composable\nfun SampleTilePreview() {\n    val context = LocalContext.current\n\n    val tileState = remember { SampleTileRenderer.TileState(0) }\n\n    val resourceState = remember {\n        val image =\n            BitmapFactory.decodeResource(context.resources, R.drawable.ic_uamp).toImageResource()\n        SampleTileRenderer.ResourceState(image)\n    }\n\n    val renderer = remember {\n        SampleTileRenderer(context)\n    }\n\n    TileLayoutPreview(\n        tileState,\n        resourceState,\n        renderer\n    )\n}\n

"},{"location":"compose-tools/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-compose-tools:<version>\"\n}\n
"},{"location":"contributing/","title":"How to Contribute","text":"

We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow.

If you find a common problem that you think would help other Wear developers please consider submitting a PR. Please avoid significant work before raising an issue https://github.com/google/horologist/issues with the label \"Feature Request\"

"},{"location":"contributing/#development","title":"Development","text":"

The project should work immediately from a fresh checkout in Android Studio (Stable or newer) or Gradle (./gradlew).

When submitting a PR, please check API compatibility and lint rules first.

A good first step is

./gradlew spotlessApply spotlessCheck compileDebugSources compileReleaseSources metalavaGenerateSignature metalavaGenerateSignatureRelease lintDebug\n

Also make sure you have (Git LFS) installed.

If you change any code affecting screenshot tests, then run the following to update changed images on a Linux host. Alternatively uncomment the same property in gradle.properties.

./gradlew testDebug -P screenshot.record=repair\n

This can be automated For a PR against the same branch, the Github action should commit against PR if possible. For maintainers, this means creating the branch in the google/horologist repo. For contributors, once your PR fails, create a second PR in your fork, that will fix the issue.

"},{"location":"contributing/#contributor-license-agreement","title":"Contributor License Agreement","text":"

Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution, this simply gives us permission to use and redistribute your contributions as part of the project. Head over to https://cla.developers.google.com/ to see your current agreements on file or to sign a new one.

You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again.

"},{"location":"contributing/#code-reviews","title":"Code reviews","text":"

All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult GitHub Help for more information on using pull requests.

"},{"location":"contributing/#translation-and-localization","title":"Translation and localization","text":"

This project uses a semi-automatic pipeline to translate strings. When new or updated localized strings are ready, a PR is generated (example: google/horologist#692). Only the files configured via localization.bzl are sent for translation.

If you see a problem with translated text, don't edit localized resource files (e.g. res/values-en/strings.xml) manually, as they'll be overwritten. Instead, file an issue and use the l10n label. This will then be forwarded to the relevant teams.

"},{"location":"contributing/#project-direction-and-ownership","title":"Project Direction and Ownership","text":"

There are a couple of reasons we may not accept an otherwise valuable contribution.

  • Where the internal framework feature team, thinks the contribution is against the long term direction of the library.
  • Where long term ownership is unclear, such as a large contribution that likely involves ongoing maintenance.
"},{"location":"datalayer-helpers-guide/","title":"DataLayer helpers libraries","text":"

These libraries provides an easy means to detect and install your app across both watch and phone.

However, they are not intended to cover complex use cases, or complex interactions between watch and phone.

"},{"location":"datalayer-helpers-guide/#getting-started","title":"Getting started","text":"
  1. Include the necessary dependency:

    dependencies {\n    implementation \"com.google.android.horologist:horologist-datalayer-watch:<version>\"\n}\n

    and

    dependencies {\n    implementation \"com.google.android.horologist:horologist-datalayer-phone:<version>\"\n}\n

    For your watch and phone projects respectively.

  2. Initialize the client, including passing a WearDataLayerRegistry.

    val appHelper = WearDataLayerAppHelper(context, wearDataLayerRegistry, scope)\n\n// or\nval appHelper = PhoneDataLayerAppHelper(context, wearDataLayerRegistry)\n
"},{"location":"datalayer-helpers-guide/#typical-use-cases","title":"Typical use cases:","text":"
  1. Connection and installation status

    This is something that your app may do from time to time, or on start up.

    val connectedNodes = appHelper.connectedNodes()\n

    The resulting list might will contain entries such as:

    AppHelperNodeStatus(\n    id=7cd1c38a,\n    displayName=Google Pixel Watch,\n    appInstallationStatus=Installed(nodeType=WATCH),\n    surfacesInfo=# SurfacesInfo@125fcbff\n        complications {\n            instance_id: 1234\n            name: \"MyComplication\"\n            timestamp {\n                nanos: 738000000\n                seconds: 1680015523\n            }\n            type: \"SHORT_TEXT\"\n        }\n        tiles {\n            name: \"MyTile\"\n            timestamp {\n                nanos: 364000000\n                seconds: 1680016845\n            }\n        }\n)\n
  2. Responding to availability change

    Once you've established the app on both devices, you may wish to respond to when the partner device connects or disconnects. For example, you may only want to show a \"launch workout\" button on the phone when the watch is connected.

    val nodes by appHelper.connectedAndInstalledNodes\n    .collectAsStateWithLifecycle()\n
  3. Installing the app on the other device

    Where the app isn't installed on the other device - be that phone or watch - then the library offers a one step option to launch installation:

    appHelper.installOnNode(node.id)\n
  4. Launching the app on the other device

    If the app is installed on the other device, you can launch it remotely:

    val result = appHelper.startRemoteOwnApp(node.id)\n
  5. Launching a specific activity on the other device

    In addition to launching your own app, you may wish to launch a different activity as part of the user journey:

    val config = activityConfig { \n    packageName = \"com.example.myapp\"\n    classFullName = \"com.example.myapp.MyActivity\"\n}\nappHelper.startRemoteActivity(node.id, config)\n
  6. Launching the companion app

    In some cases, it can be useful to launch the companion app, either from the watch or the phone.

    For example, if the connected device does not have your Tile installed, you may wish to offer the user the option to navigate to the companion app to install it:

    if (node.surfacesInfo.tilesList.isEmpty() && askUserAttempts < MAX_ATTEMPTS) {\n    // Show guidance to the user and then launch companion\n    // to allow the to install the Tile.\n    val result = appHelper.startCompanion(node.id)\n}\n
  7. Tracking Tile installation (Wear-only)

    To determine whether your Tile(s) are installed, add the following to your TileService:

    In onTileAddEvent:

    wearAppHelper.markTileAsInstalled(\"SummaryTile\")\n

    In onTileRemoveEvent:

    wearAppHelper.markTileAsRemoved(\"SummaryTile\")\n
  8. Tracking Complication installation (Wear-only)

    To determine whether your Complication(s) are in-use, add the following to your ComplicationDataSourceService:

    In onComplicationActivated:

    wearAppHelper.markComplicationAsActivated(\"GoalsComplication\")\n

    In onComplicationDeactivated:

    wearAppHelper.markComplicationAsDeactivated(\"GoalsComplication\")\n
  9. Tracking the main activity has been launched at least once (Wear-only)

To determine if your main activity has been launched once, use:

kotlin wearAppHelper.markActivityLaunchedOnce()

  1. Tracking the app has been set up (Wear-only)

    To mark that the user has completed in the app the necessary setup steps such that it is ready for use, use the following:

    wearAppHelper.markSetupComplete()\n

    And when the app is no longer considered in a fully setup state, use the following:

    wearAppHelper.markSetupNoLongerComplete()\n
"},{"location":"datalayer/","title":"DataLayer library","text":"

DataStore documentation https://developer.android.com/topic/libraries/architecture/datastore

Direct DataLayer sample code https://github.com/android/wear-os-samples

"},{"location":"datalayer/#datalayer-approach","title":"DataLayer approach.","text":"

The Horologist DataLayer libraries, provide common abstractions on top of the Wearable DataLayer. These are built upon a common assumption of Google Protobuf and gRPC, which allows sharing data definitions throughout your Wear and Mobile apps.

See this gradle build file for an example of configuring a build to use proto definitions.

syntax = \"proto3\";\n\nmessage CounterValue {\n  int64 value = 1;\n  .google.protobuf.Timestamp updated = 2;\n}\n\nmessage CounterDelta {\n  int64 delta = 1;\n}\n\nservice CounterService {\n  rpc getCounter(.google.protobuf.Empty) returns (CounterValue);\n  rpc increment(CounterDelta) returns (CounterValue);\n}\n
"},{"location":"datalayer/#registering-serializers","title":"Registering Serializers.","text":"

The WearDataLayerRegistry is an application singleton to register the Serializers.

object CounterValueSerializer : Serializer<CounterValue> {\n    override val defaultValue: CounterValue\n        get() = CounterValue.getDefaultInstance()\n\n    override suspend fun readFrom(input: InputStream): CounterValue =\n        try {\n            CounterValue.parseFrom(input)\n        } catch (exception: InvalidProtocolBufferException) {\n            throw CorruptionException(\"Cannot read proto.\", exception)\n        }\n\n    override suspend fun writeTo(t: CounterValue, output: OutputStream) {\n        t.writeTo(output)\n    }\n}\n\nval registry = WearDataLayerRegistry.fromContext(\n    application = sampleApplication,\n    coroutineScope = coroutineScope,\n).apply {\n    registerSerializer(CounterValueSerializer)\n}\n
"},{"location":"datalayer/#use-androidx-datastore","title":"Use Androidx DataStore","text":"

This library provides a new implementation of Androidx DataStore, in addition to the local Proto and Preferences implementations. The implementation uses the Wearable DataClient with a single owner and multiple readers.

See DataStore.

"},{"location":"datalayer/#publishing-a-datastore","title":"Publishing a DataStore","text":"
private val dataStore: DataStore<CounterValue> by lazy {\n  registry.protoDataStore<CounterValue>(lifecycleScope)\n}\n
"},{"location":"datalayer/#reading-a-remote-datastore","title":"Reading a remote DataStore","text":"
val counterFlow = registry.protoFlow<CounterValue>(TargetNodeId.PairedPhone)\n
"},{"location":"datalayer/#using-grpc","title":"Using gRPC","text":"

This library implements the gRPC transport over the Wearable MessageClient using the RPC request feature.

"},{"location":"datalayer/#implementing-a-service","title":"Implementing a service","text":"
class CounterService(val dataStore: DataStore<GrpcDemoProto.CounterValue>) :\n    CounterServiceGrpcKt.CounterServiceCoroutineImplBase() {\n        override suspend fun getCounter(request: Empty): GrpcDemoProto.CounterValue {\n            return dataStore.data.first()\n        }\n\n        override suspend fun increment(request: GrpcDemoProto.CounterDelta): GrpcDemoProto.CounterValue {\n            return dataStore.updateData {\n                it.copy {\n                    this.value = this.value + request.delta\n                    this.updated = System.currentTimeMillis().toProtoTimestamp()\n                }\n            }\n        }\n    }\n\nclass WearCounterDataService : BaseGrpcDataService<CounterServiceGrpcKt.CounterServiceCoroutineImplBase>() {\n\n    private val dataStore: DataStore<CounterValue> by lazy {\n        registry.protoDataStore<CounterValue>(lifecycleScope)\n    }\n\n    override val registry: WearDataLayerRegistry by lazy {\n        WearDataLayerRegistry.fromContext(\n            application = applicationContext,\n            coroutineScope = lifecycleScope,\n        ).apply {\n            registerSerializer(CounterValueSerializer)\n        }\n    }\n\n    override fun buildService(): CounterServiceGrpcKt.CounterServiceCoroutineImplBase {\n        return CounterService(dataStore)\n    }\n}\n
"},{"location":"datalayer/#calling-a-remote-service","title":"Calling a remote service","text":"
val client = registry.grpcClient(\n    nodeId = TargetNodeId.PairedPhone,\n    coroutineScope = sampleApplication.servicesCoroutineScope,\n) {\n    CounterServiceGrpcKt.CounterServiceCoroutineStub(it)\n}\n\n// Call the increment method from the proto service definition\nval newValue: CounterValue =\n    counterService.increment(counterDelta { delta = i.toLong() })\n
"},{"location":"datalayer/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-datalayer:<version>\"\n}\n
"},{"location":"media-data/","title":"Media Data library","text":"

This library contains the implementation of the repositories defined in the media domain library, using Media3 and an internal database as data sources.

It also exposes its data sources classes so they can be used by your custom repositories.

"},{"location":"media-data/#mediadownloadservice","title":"MediaDownloadService","text":"

An implementation of Media3\u2019s DownloadService that, in conjunction with auxiliary classes, will monitor the state and progress of media downloads, and update the information in the internal database.

"},{"location":"media-data/#usage","title":"Usage","text":"
  1. Add your own implementation of the service, extending MediaDownloadService;

  2. Add your service implementation to your app\u2019s AndroidManifest.xml:

    <service android:name=\"MediaDownloadServiceImpl\"\n    android:exported=\"false\">\n    <intent-filter>\n        <action android:name=\"com.google.android.exoplayer.downloadService.action.RESTART\"/>\n        <category android:name=\"android.intent.category.DEFAULT\"/>\n    </intent-filter>\n</service>\n

"},{"location":"media-data/#media-toolkit-implementation","title":"Media Toolkit implementation","text":""},{"location":"media-data/#downloadmanagerlistener","title":"DownloadManagerListener","text":"

The DownloadManagerListener is an implementation of listener for DownloadManager events which can also get notified of DownloadService creation and destruction events. This class persists information such as the id and the download status in the local database MediaDownloadLocalDataSource.

override fun onDownloadChanged(\n        downloadManager: DownloadManager,\n        download: Download,\n        finalException: Exception?\n    ) {\n        coroutineScope.launch {\n            val mediaId = download.request.id\n            val status = MediaDownloadEntityStatusMapper.map(download.state)\n\n            if (status == MediaDownloadEntityStatus.Downloaded) {\n                mediaDownloadLocalDataSource.setDownloaded(mediaId)\n            } else {\n                mediaDownloadLocalDataSource.updateStatus(mediaId, status)\n            }\n        }\n\n        downloadProgressMonitor.start(downloadManager)\n    }\n\noverride fun onDownloadRemoved(downloadManager: DownloadManager, download: Download) {\n        coroutineScope.launch {\n            val mediaId = download.request.id\n            mediaDownloadLocalDataSource.delete(mediaId)\n        }\n    }\n

"},{"location":"media-data/#downloadprogressmonitor","title":"DownloadProgressMonitor","text":"

The DownloadProgressMonitor monitors the status of the download by polling the DownloadManager and persists the progress in the local database MediaDownloadLocalDataSource.

    private fun update(downloadManager: DownloadManager) {\n        coroutineScope.launch {\n            val downloads = mediaDownloadLocalDataSource.getAllDownloading()\n\n            if (downloads.isNotEmpty()) {\n                for (it in downloads) {\n                    downloadManager.downloadIndex.getDownload(it.mediaId)?.let { download ->\n                        mediaDownloadLocalDataSource.updateProgress(\n                            mediaId = download.request.id,\n                            progress = download.percentDownloaded\n                                .coerceAtLeast(DOWNLOAD_PROGRESS_START),\n                            size = download.contentLength\n                        )\n                    }\n                }\n            } else {\n                stop()\n            }\n        }\n\n        if (running) {\n            handler.removeCallbacksAndMessages(null)\n            handler.postDelayed({ update(downloadManager) }, UPDATE_INTERVAL_MILLIS)\n        }\n    }\n
"},{"location":"media-data/#ui-implementation","title":"UI Implementation","text":"

The PlaylistsDownloadScreen is composed by the MediaContent and the ButtonContent. The MediaContent displays the content that is being downloaded or already downloaded. For content that is being downloaded, we display the download progress for each track in place of the artist name in the secondaryLabel

val secondaryLabel = when (downloadMediaUiModel) {\n        is DownloadMediaUiModel.Downloading -> {\n            when (downloadMediaUiModel.progress) {\n                is DownloadMediaUiModel.Progress.Waiting -> stringResource(\n                    id = R.string.horologist_playlist_download_download_progress_waiting\n                )\n\n                is DownloadMediaUiModel.Progress.InProgress -> when (downloadMediaUiModel.size) {\n                    is DownloadMediaUiModel.Size.Known -> {\n                        val size = Formatter.formatShortFileSize(\n                            LocalContext.current,\n                            downloadMediaUiModel.size.sizeInBytes\n                        )\n                        stringResource(\n                            id = R.string.horologist_playlist_download_download_progress_known_size,\n                            downloadMediaUiModel.progress.progress,\n                            size\n                        )\n                    }\n\n                    DownloadMediaUiModel.Size.Unknown -> stringResource(\n                        id = R.string.horologist_playlist_download_download_progress_unknown_size,\n                        downloadMediaUiModel.progress.progress\n                    )\n                }\n            }\n        }\n\n        is DownloadMediaUiModel.Downloaded -> downloadMediaUiModel.artist\n        is DownloadMediaUiModel.NotDownloaded -> downloadMediaUiModel.artist\n    }\n

The ButtonContent displays either a download chip or three buttons to delete, shuffle and play the content. While content is being downloaded, we show an animation on the first button on the left to track progress .

            if (state.downloadMediaListState == PlaylistDownloadScreenState.Loaded.DownloadMediaListState.None) {\n                if (state.downloadsProgress is DownloadsProgress.InProgress) {\n                    StandardChip(\n                        label = stringResource(id = R.string.horologist_playlist_download_button_cancel),\n                        onClick = { onCancelDownloadButtonClick(state.collectionModel) },\n                        modifier = Modifier.padding(bottom = 16.dp),\n                        icon = Icons.Default.Close\n                    )\n                } else {\n                    StandardChip(\n                        label = stringResource(id = R.string.horologist_playlist_download_button_download),\n                        onClick = { onDownloadButtonClick(state.collectionModel) },\n                        modifier = Modifier.padding(bottom = 16.dp),\n                        icon = Icons.Default.Download\n                    )\n                }\n            } else {\n                Row(\n                    modifier = Modifier\n                        .padding(bottom = 16.dp)\n                        .height(52.dp),\n                    verticalAlignment = CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(6.dp, CenterHorizontally)\n                ) {\n                    FirstButton(\n                        downloadMediaListState = state.downloadMediaListState,\n                        downloadsProgress = state.downloadsProgress,\n                        collectionModel = state.collectionModel,\n                        onDownloadButtonClick = onDownloadButtonClick,\n                        onCancelDownloadButtonClick = onCancelDownloadButtonClick,\n                        onDownloadCompletedButtonClick = onDownloadCompletedButtonClick,\n                        modifier = Modifier\n                            .weight(weight = 0.3F, fill = false)\n                    )\n\n                    StandardButton(\n                        imageVector = Icons.Default.Shuffle,\n                        contentDescription = stringResource(id = R.string.horologist_playlist_download_button_shuffle_content_description),\n                        onClick = { onShuffleButtonClick(state.collectionModel) },\n                        modifier = Modifier\n                            .weight(weight = 0.3F, fill = false)\n                    )\n\n                    StandardButton(\n                        imageVector = Icons.Filled.PlayArrow,\n                        contentDescription = stringResource(id = R.string.horologist_playlist_download_button_play_content_description),\n                        onClick = { onPlayButtonClick(state.collectionModel) },\n                        modifier = Modifier\n                            .weight(weight = 0.3F, fill = false)\n                    )\n                }\n            }\n
"},{"location":"media-data/#result","title":"Result","text":"

Download screens:

"},{"location":"media-playerscreen/","title":"Stateful PlayerScreen guide","text":"

This is a guide on how to use the stateful PlayerScreen with your own implementation of PlayerRepository.

"},{"location":"media-playerscreen/#basic-usage","title":"Basic usage","text":""},{"location":"media-playerscreen/#1-implement-playerrepository","title":"1. Implement PlayerRepository","text":"
class PlayerRepositoryImpl : PlayerRepository {\n    // implement required properties and functions\n}\n

In the sample implementation below, the repository listens to the events of Media3's Player and update its property values accordingly (see onIsPlayingChanged). Its operations are also called on the Player (see setPlaybackSpeed).

class PlayerRepositoryImpl(\n    private val player: Player\n) : PlayerRepository {\n\n    private val _currentState: MutableStateFlow<PlayerState> = MutableStateFlow(PlayerState.Idle)\n    override val currentState: StateFlow<PlayerState> = _currentState\n\n    private val listener = object : Player.Listener {\n\n        override fun onIsPlayingChanged(isPlaying: Boolean) {\n            _currentState.value = if (isPlaying) { \n                PlayerState.Playing\n            } else {\n                PlayerState.Ready\n            }\n        }\n    }\n\n    init {\n        player.add(listener)\n    }\n\n    fun setPlaybackSpeed(speed: Float) { \n        player.setPlaybackSpeed(speed) \n    }\n}\n
"},{"location":"media-playerscreen/#2-extend-playerviewmodel","title":"2. Extend PlayerViewModel","text":"

Pass your implementation of PlayerRepository as constructor parameter.

class MyCustomViewModel(\n    playerRepository: PlayerRepositoryImpl\n): PlayerViewModel(playerRepository) {\n    // add custom implementation\n}\n
"},{"location":"media-playerscreen/#3-add-playerscreen","title":"3. Add PlayerScreen","text":"

Pass your PlayerViewModel extension as value to the constructor parameter.

PlayerScreen(playerViewModel = myCustomViewModel)\n
"},{"location":"media-playerscreen/#class-diagram","title":"Class diagram","text":"

The following diagram shows the interactions between the classes.

"},{"location":"media-sample/","title":"Media sample app","text":"

The goal of this sample is to show how to implement an audio media app for Wear OS, using the Horologist media libraries, following the design principles described in Considerations for media apps .

The app supports listening to downloaded music. It loads a music catalog from a remote server and allows the user to browse the albums and songs. Tapping on a song will play it through connected speakers or headphones. Under the hood it uses Media3.

"},{"location":"media-sample/#features","title":"Features","text":"

The app showcases the implementation of the following features:

  • Media playback, restricted to paired Bluetooth devices
  • Launch of Bluetooth settings to connect devices for media playback
  • Volume control
  • Radial background based on media artwork color palette
  • Download media
  • API sync with WorkManager
  • Network rules
  • Splash screen
  • Marquee text for song titles
  • Tiles

This list is not exhaustive.

"},{"location":"media-sample/#audio","title":"Audio","text":"

Music provided by the Free Music Archive.

  • Wake Up by The Kyoto Connection.

Recordings provided by the Ambisonic Sound Library.

  • Pre Game Marching Band by Watson Wu
  • Chickens on a Farm by Watson Wu
  • Rural Market Busker by Stephan Schutze
  • Steamtrain Interior by Stephan Schutze
  • Rural Road Car Pass by Stephan Schutze
  • 10 Feet from Shore by Watson Wu
"},{"location":"media-toolkit/","title":"Media Toolkit","text":""},{"location":"media-toolkit/#overview","title":"Overview","text":"

Horologist provides what it is called the \"Media Toolkit\": a set of libraries to build media apps on Wear OS and a sample app that you can run to see the toolkit in action.

The following modules in the Horologist project are part of the toolkit:

  • media-ui: common media UI components and screens like PlayerScreen.
  • media: domain model for media related functionality. Provides an abstraction to the UI module (media-ui) that is agnostic to the Player implementation.
  • media-data: implementation of the domain module (media) using Media3.
  • media3-backend: Player on top of Media3 including functionalities such as avoiding playing music on the watch speaker.
  • media-sample: sample app to listen to downloaded music.
"},{"location":"media-toolkit/#architecture-overview","title":"Architecture overview","text":"

The Media Toolkit libraries are separated by layers (UI, domain and data) following the recommended app architecture .

The reason for including a domain layer is to provide flexibility to projects to use the UI library or the data library independently.

For example, if your project already contains an implementation for the player and you are only interested in using the media screens provided by the toolkit, then only the UI library needs to be added as a dependency. Thus, no extra dependencies ( e.g. Media3) will be added to your project.

On the other hand, if your project does not need any of the media screens or media UI components provided by the UI library, and you are only interested in the player implementation, then only the data library needs to be added as a dependency to your project.

"},{"location":"media-toolkit/#getting-started","title":"Getting started","text":"

The usage of the toolkit will vary according to the requirements of your project.

As per architecture overview, your project might not need to add all the libraries of the toolkit as dependency. If that\u2019s the case, refer to the documentation of each library required to your project for a guide on how to get started.

For a walkthrough on how to build a very simple media application using some libraries of the toolkit, refer to this guide.

For good reference on how to use all the libraries available in the toolkit, refer to the code of the media-sample app.

"},{"location":"media-ui/","title":"Media UI library","text":"

This library contains a set of composables for media player apps:

Individual controls like Play, Pause and Seek buttons; Components that might combine multiple controls, like PlayPauseButton and MediaControlButtons; Screens, like PlayerScreen, BrowseScreen and EntityScreen.

The previews of the composables can be found in the debug folder of the module source code.

This library is not dependent on any specific player implementation as per architecture overview.

"},{"location":"media-ui/#stateful-components","title":"Stateful components","text":"

Most of the components available in this library contain an overloaded version of themselves which accept either a UI model (MediaUiModel, PlaylistUiModel) or PlayerUiState or PlayerViewModel as parameters. We call those versions \u201cstateful components\u201d, which is a different definition from the compose documentation .

While the stateless components provide full customization, the stateful components provide convenience (if the default implementation suits your project requirements), as can be seen in the example below.

Stateless PodcastControlButtons usage:

PodcastControlButtons(\n    onPlayButtonClick = { },\n    onPauseButtonClick = { },\n    playPauseButtonEnabled = true,\n    playing = false,\n    percent = 0f,\n    onSeekBackButtonClick = { },\n    seekBackButtonEnabled = true,\n    onSeekForwardButtonClick = { },\n    seekForwardButtonEnabled = true,\n)\n

Stateful PodcastControlButtons usage:

PodcastControlButtons(\n    playerViewModel = viewModel,\n    playerUiState = playerUiState,\n)\n

Further examples on how to use these components can be found in the Stateful PlayerScreen guide.

"},{"location":"media/","title":"Media Domain library","text":"

This library currently contains a set of models and repositories that are common to media apps. But it can be expanded in the future to also include common use cases, as per domain layer guide.

The data and domain layer guides seem to imply that the definitions of the repositories should belong to the data layer. This would make the domain library dependent on a specific data library. In this project, the repositories are defined ( not implemented) in the domain layer. This makes the domain layer independent of external layers, and any implementation of the repositories can be used: the ones provided by the toolkit, or custom implementations provided by your project.

The reason for having a domain library is described in the architecture overview.

"},{"location":"media3-backend/","title":"Wear Media3 Backend library","text":""},{"location":"media3-backend/#features","title":"Features","text":"

The media3-backend module implements many of the suggested approaches at https://developer.android.com/training/wearables/overlays/audio. These enforce the best practices that will make you app work well for users on a range of Wear OS devices.

These build on top of the Media3 player featuring ExoPlayer which is optimised for Wear Playback, and is the standard playback engine for Wear OS media apps.

All functionality is demonstrated in the media-sample app, and as such is not described here with extensive code samples.

"},{"location":"media3-backend/#bluetooth-headphone-connections","title":"Bluetooth Headphone Connections","text":"

Any extended playback such as music, podcasts, or radio should only use a connected bluetooth speaker. See https://developer.android.com/training/wearables/principles#bluetooh-headphones for principles applying to Media and Wear OS apps generally.

When not connected, the default Watch Speaker will be used and so this must be actively avoided.

Horologist provides the PlaybackRules abstraction that allows you to intercept playback requests through your UI, a system media Tile, pressing a bluetooth headphone, or other services such as assistant.

public object Normal : PlaybackRules {\n    /**\n     * Can the given item be played with it's given state.\n     */\n    override suspend fun canPlayItem(mediaItem: MediaItem): Boolean = true\n\n    /**\n     * Can Media be played with the given audio target.\n     */\n    override fun canPlayWithOutput(audioOutput: AudioOutput): Boolean =\n        audioOutput is AudioOutput.BluetoothHeadset\n}\n

The WearConfiguredPlayer wraps the ExoPlayer to avoid starting playback and also pause immediately if the headset becomes disconnected. It will prompt the user to connect a headset at this point.

The AudioOutputSelector and default implementation BluetoothSettingsOutputSelector are used to prompt the user to connect a Bluetooth headset and then continue playback once connected.

public interface AudioOutputSelector {\n    /**\n     * Change from the current audio output, according to some sensible logic,\n     * and return when either the user has selected a new audio output or returning null\n     * if timed out.\n     */\n    public suspend fun selectNewOutput(currentAudioOutput: AudioOutput): AudioOutput?\n}\n
"},{"location":"media3-backend/#audio-offload","title":"Audio Offload","text":"

In line with https://exoplayer.dev/battery-consumption.html#audio-playback, Audio Offload allows your app to playback audio while in the background without waking up. This dramatically improves the users battery life, as well as decreasing the occurrences of Audio Underruns.

The AudioOffloadManager configures and controls Audio Offload, enabling sleeping while your app is in the background and disabling while in the foreground.

"},{"location":"media3-backend/#logging","title":"Logging","text":"

The media3-backend module interacts with ExoPlayer instance, but many events may be required for error handling, logging or metrics. Your can register your own Player.Listener with the ExoPlayer instance, but to receive generally useful events you can implement ErrorReporter to receive events and report with Android Log or write to a database.

Other things in the Horologist media libs will report events, and they all consistently use ErrorReporter to allow you to understand all activity in your app.

public interface ErrorReporter {\n    public fun logMessage(\n        message: String,\n        category: Category = Category.Unknown,\n        level: Level = Level.Info\n    )\n}\n
"},{"location":"media3-backend/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-media3-backend:<version>\"\n}\n
"},{"location":"network-awareness/","title":"Network Awareness library","text":""},{"location":"network-awareness/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-network-awareness:<version>\"\n}\n
"},{"location":"network-awareness/#problem-statement","title":"Problem Statement","text":"

On Wear choice of network is critical to efficient applications. See https://developer.android.com/training/wearables/data/network-access for more information.

The default behaviour is roughly

  • Use Paired Bluetooth connection when it is available.
  • Use Wifi when available and Charging.
  • Use LTE if no other network is available.
  • Wifi and Cell may be requested specifically by the application.

This leads to some suboptimal decisions

  • Downloading large downloads over the slow and shared bluetooth connection. This may overload the connection and starve other applications. Instead, Wifi or Cellular would be better used to quickly download and then close the connection.
  • Using LTE for trivial requests when not really required.
  • Using a single current network for all traffic regardless of length or important of the request.
"},{"location":"network-awareness/#functionality","title":"Functionality","text":"

This library allows defining rules based on the RequestType and NetworkType, currently integrated into OkHttp.

public interface NetworkingRules {\n    /**\n     * Is this request considered high bandwidth and should activate LTE or Wifi.\n     */\n    public fun isHighBandwidthRequest(requestType: RequestType): Boolean\n\n    /**\n     * Checks whether this request is allowed on the current network type.\n     */\n    public fun checkValidRequest(\n        requestType: RequestType,\n        currentNetworkInfo: NetworkInfo\n    ): RequestCheck\n\n    /**\n     * Returns the preferred network for a request.\n     *\n     * Null means no suitable network.\n     */\n    public fun getPreferredNetwork(\n        networks: Networks,\n        requestType: RequestType\n    ): NetworkStatus?\n}\n

It allow allows logging network usage and visibility into network status.

See media-sample for an example. Key classes to observe usage of

  • UampNetworkingRules - defining the app specific rules for the network.
  • MediaInfoTimeText - An example of displaying network status and usage to the user.
  • NetworkAwareCallFactory - Used to wrap an OkHttp Call.Factory when passed to a library such as Coil.
  • NetworkSelectingCallFactory - Used to apply the networking rules to OkHttp.
  • NetworkRepository - The Repository for the current network state.
  • HighBandwidthNetworkMediator - a mediator for requesting high bandwidth networks.
"},{"location":"simple-media-app-guide/","title":"Build a simple media player app","text":"

This guide will walk you through on how to build a very simple media player app for Wear OS, capable of playing a media which is hosted on the internet.

This guide assumes that you are familiar with:

  • How to create Wear OS projects in Android Studio;
  • Kotlin programming language;
  • Jetpack Compose;
"},{"location":"simple-media-app-guide/#display-a-playerscreen","title":"Display a PlayerScreen","text":""},{"location":"simple-media-app-guide/#1-add-dependency","title":"1 - Add dependency","text":"

Create a new project from Android Studio by choosing \"Basic Wear App Without Associated Tiles\" from \"Wear OS\" templates. Add dependency on media-ui to your project\u2019s build.gradle:

implementation \"com.google.android.horologist:horologist-media-ui:$horologist_version\"\n
"},{"location":"simple-media-app-guide/#2-add-playerscreen","title":"2 - Add PlayerScreen","text":"

Add the following code to your Activity\u2019s onCreate function:

setContent {\n    PlayerScreen(\n        mediaDisplay = {\n            TextMediaDisplay(\n                title = \"Song name\",\n                subtitle = \"Artist name\"\n            )\n        },\n        controlButtons = {\n            PodcastControlButtons(\n                onPlayButtonClick = { },\n                onPauseButtonClick = { },\n                playPauseButtonEnabled = true,\n                playing = false,\n                onSeekBackButtonClick = { },\n                seekBackButtonEnabled = true,\n                onSeekForwardButtonClick = { },\n                seekForwardButtonEnabled = true,\n            )\n        },\n        buttons = { }\n    )\n}\n

This code is displaying PlayerScreen on the app. PlayerScreen is a full screen composable that contains slots parameters to pass the contents to be displayed for media display, control buttons and more.

In this sample, we are using the UI components TextMediaDisplay and PodcastControlButtons, provided by the UI library, as values to parameters of PlayerScreen.

"},{"location":"simple-media-app-guide/#result","title":"Result","text":"

Run the app and you should see the following screen:

None of the controls are working, as they were not implemented yet.

"},{"location":"simple-media-app-guide/#make-the-screen-functional","title":"Make the screen functional","text":""},{"location":"simple-media-app-guide/#1-add-dependencies","title":"1 - Add dependencies","text":"

Add the following dependencies to your project\u2019s build.gradle:

implementation \"com.google.android.horologist:horologist-media-data:$horologist_version\"\nimplementation \"com.google.android.horologist:horologist-audio-ui:$horologist_version\"\nimplementation(\"androidx.media3:media3-exoplayer:$media3_version\")\n
"},{"location":"simple-media-app-guide/#2-add-viewmodel","title":"2 - Add ViewModel","text":"

Add a ViewModel extending PlayerViewModel, providing an instance of PlayerRepositoryImpl:

class MyViewModel(\n    player: Player,\n    playerRepository: PlayerRepositoryImpl = PlayerRepositoryImpl()\n) : PlayerViewModel(playerRepository) {}\n
"},{"location":"simple-media-app-guide/#3-add-init-block","title":"3 - Add init block","text":"

Add the following init block to the ViewModel to connect the Player to the PlayerRepository, set a media and update the position of the player every second:

init {\n    viewModelScope.launch {\n        playerRepository.connect(player) {}\n\n        playerRepository.setMedia(\n            Media(\n                id = \"wake_up_02\",\n                uri = \"https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/02_-_Geisha.mp3\",\n                title = \"Geisha\",\n                artist = \"The Kyoto Connection\"\n            )\n        )\n    }\n}\n
"},{"location":"simple-media-app-guide/#4-create-an-instance-of-the-viewmodel","title":"4 - Create an instance of the ViewModel","text":"

Change your Activity\u2019s onCreate function to:

@SuppressLint(\"UnsafeOptInUsageError\")\nval player = ExoPlayer.Builder(this)\n    .setSeekForwardIncrementMs(5000L)\n    .setSeekBackIncrementMs(5000L)\n    .build()\n// ViewModels should NOT be created here like this\nval viewModel = MyViewModel(player)\nval volumeViewModel = createVolumeViewModel()\n\nsetContent {\n    PlayerScreen(\n        playerViewModel = viewModel,\n        volumeViewModel = volumeViewModel,\n        mediaDisplay = { playerUiState: PlayerUiState ->\n          DefaultMediaInfoDisplay(playerUiState)\n        },\n        controlButtons = { playerUIController: PlayerUiController,\n                           playerUiState: PlayerUiState ->\n          PodcastControlButtons(\n                  playerController = playerUIController,\n                  playerUiState = playerUiState\n          )\n        },\n        buttons = { }\n    )\n}\n

Add createVolumeViewModel function to create a VolumeViewModel:

fun createVolumeViewModel(): VolumeViewModel {\n    val audioRepository = SystemAudioRepository.fromContext(application)\n    val vibrator: Vibrator = application.getSystemService(Vibrator::class.java)\n    return VolumeViewModel(audioRepository, audioRepository, onCleared = {\n        audioRepository.close()\n    }, vibrator)\n}\n

We are creating an instance of ExoPlayer, passing it to the ViewModel.

Then for the PlayerScreen slots we are using:

  • the DefaultMediaDisplay component, which accepts a MediaUiModel instance as parameter;
  • the stateful version of PodcastControlButtons, which accepts instances of PlayerViewModel and PlayerUiStateas parameters to hook the controls with the ViewModel;
"},{"location":"simple-media-app-guide/#result_1","title":"Result","text":"

Run the app again and this time, play with the screen controls as the app should be able to play, pause, and seek the media now:

"},{"location":"tiles/","title":"Tiles Library","text":""},{"location":"tiles/#suspendingtileservice","title":"SuspendingTileService","text":"

Provides a SuspendingTileService, which also acts as a LifecycleService.

class ExampleTileService : SuspendingTileService() {\n    override suspend fun tileRequest(requestParams: RequestBuilders.TileRequest): Tile {\n        return Tile.Builder()\n            // create your tile here\n            .build()\n    }\n\n    override suspend fun resourcesRequest(\n        requestParams: RequestBuilders.ResourcesRequest\n    ): ResourceBuilders.Resources = ResourceBuilders.Resources.Builder().setVersion(\"1\").build()\n}\n
"},{"location":"tiles/#coil-image-helpers","title":"Coil Image Helpers","text":"

Provides a suspending method to load an image from the network, convert to an RGB_565 bitmap, and encode as a Tiles InlineImageResource.

val imageResource = imageLoader.loadImageResource(applicationContext, \n    \"https://media.githubusercontent.com/media/google/horologist/main/docs/media-ui/playerscreen.png\") {\n    // Show a local error image if missing\n    error(R.drawable.missingImage)\n}\n
"},{"location":"tiles/#download","title":"Download","text":"
repositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation \"com.google.android.horologist:horologist-tiles:<version>\"\n}\n
"},{"location":"updating-old/","title":"Updating & releasing Horologist","text":"

This guide is currently not in use. See updating.md instead.

This doc is mostly for maintainers.

"},{"location":"updating-old/#new-features-bugfixes","title":"New features & bugfixes","text":"

All new features should be uploaded as PRs against the main branch.

Once merged into main, they will be automatically merged into the snapshot branch.

"},{"location":"updating-old/#jetpack-compose-snapshots","title":"Jetpack Compose Snapshots","text":"

We publish snapshot versions of Horologist, which depend on a SNAPSHOT versions of Jetpack Compose. These are built from the snapshot branch.

"},{"location":"updating-old/#updating-to-a-newer-compose-snapshot","title":"Updating to a newer Compose snapshot","text":"

As mentioned above, updating to a new Compose snapshot is done by submitting a new PR against the snapshot branch:

git checkout snapshot && git pull\n# Create branch for PR\ngit checkout -b update_snapshot\n

Now edit the project to depend on the new Compose SNAPSHOT version:

Edit /gradle/libs.versions.toml:

Under [versions]:

  1. Update the composesnapshot property to be the snapshot number
  2. Ensure that the compose property is correct

Make sure the project builds and test pass:

./gradlew check\n

Now git commit the changes and push to GitHub.

Finally create a PR (with the base branch as snapshot) and send for review.

"},{"location":"updating-old/#releasing","title":"Releasing","text":"

Once the next Jetpack Compose version is out, we're ready to push a new release:

"},{"location":"updating-old/#1-merge-snapshot-into-main","title":"#1: Merge snapshot into main","text":"

First we merge the snapshot branch into main:

git checkout snapshot && git pull\ngit checkout main && git pull\n\n# Create branch for PR\ngit checkout -b main_snapshot_merge\n\n# Merge in the snapshot branch\ngit merge snapshot\n
"},{"location":"updating-old/#2-update-dependencies","title":"#2: Update dependencies","text":"

Edit /gradle/libs.versions.toml:

Under [versions]:

  1. Update the composesnapshot property to a single character (usually -). This disables the snapshot repository.
  2. Update the compose property to match the new release (i.e. 1.0.0-beta06)

Make sure the project builds and test pass:

./gradlew check\n

Commit the changes.

"},{"location":"updating-old/#3-bump-the-version-number","title":"#3: Bump the version number","text":"

Edit gradle.properties:

  • Update the VERSION_NAME property and remove the -SNAPSHOT suffix.

Commit the changes, using the commit message containing the new version name.

"},{"location":"updating-old/#4-push-to-github","title":"#4: Push to GitHub","text":"

Push the branch to GitHub and create a PR against the main branch, and send for review. Once approved and merged, it will be automatically deployed to Maven Central.

"},{"location":"updating-old/#5-create-release","title":"#5: Create release","text":"

Once the above PR has been approved and merged, we need to create the GitHub release:

  • Open up the Releases page.
  • At the top you should see a 'Draft' release, auto populated with any PRs since the last release. Click 'Edit'.
  • Make sure that the version number matches what we released (the tool guesses but is not always correct).
  • Double check everything, then press 'Publish release'.

At this point the release is published. This will trigger the docs action to run, which will auto-deploy a new version of the website.

"},{"location":"updating-old/#6-prepare-the-next-development-version","title":"#6: Prepare the next development version","text":"

The current release is now finished, but we need to update the version for the next development version:

Edit gradle.properties:

  • Update the VERSION_NAME property, by increasing the version number, and adding the -SNAPSHOT suffix.
  • Example: released version: 0.3.0. Update to 0.3.1-SNAPSHOT

git commit and push to main.

Finally, merge all of these changes back to snapshot:

git checkout snapshot && git pull\ngit merge main\ngit push\n
"},{"location":"updating/","title":"Updating & releasing Horologist","text":"

This doc is mostly for maintainers.

Ensure your Sonatype JIRA credentials are set in your environment variables.

export ORG_GRADLE_PROJECT_mavenCentralUsername=username\nexport ORG_GRADLE_PROJECT_mavenCentralPassword=password\n

Decrypt the signing key to release a public build.

release/signing-setup.sh '<Horologist AES key>'\ngradlew clean publish --no-parallel --stacktrace\nrelease/signing-cleanup.sh\n

The deployment then needs to be manually released via the Nexus Repository Manager. See Releasing Deployment from OSSRH.

"},{"location":"updating/#snapshot-release","title":"Snapshot release","text":"

For a snapshot release, the signing key is not used. Ensure VERSION_NAME in gradle.properties has the -SNAPSHOT suffix or specify the version via -PVERSION_NAME=....

gradlew -PVERSION_NAME=0.0.1-SNAPSHOT clean publish --no-parallel --stacktrace\n
"},{"location":"using-snapshot-version/","title":"Using a Snapshot Version of the Library","text":"

If you would like to depend on the cutting edge version of the Horologist library, you can use the snapshot versions that are published to Sonatype OSSRH's snapshot repository. These are updated on every commit to main.

To do so:

repositories {\n    // ...\n    maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }\n}\n\ndependencies {\n    // Check the latest SNAPSHOT version from the link above\n    classpath 'com.google.android.horologist:horologist-tiles:XXX-SNAPSHOT'\n}\n

You might see a number of different versioned snapshots. If we use an example:

  • 0.3.0-SNAPSHOT is a build from the main branch, and depends on the latest tagged Jetpack Compose release (i.e. alpha03).
  • 0.3.0.compose-6574163-SNAPSHOT is a build from the snapshot branch. This depends on the SNAPSHOT build of Jetpack Compose from build 6574163. You should only use these if you are using Jetpack Compose snapshot versions (see below).
"},{"location":"using-snapshot-version/#using-jetpack-compose-snapshots","title":"Using Jetpack Compose Snapshots","text":"

If you're using SNAPSHOT versions of the androidx.compose libraries, you might run into issues with the current stable Horologist release forcing an older version of those libraries.

We publish snapshot versions of Horologist which depend on recent Jetpack Compose SNAPSHOT repositories. To find a recent build, look through the snapshot repository for any versions in the scheme x.x.x.compose-YYYY-SNAPSHOT (for example: 0.3.0.compose-6574163-SNAPSHOT). The YYYY in the scheme is the snapshot build being used from AndroidX (from the example: build 6574163). You can then use it like so:

repositories {\n    // ...\n    maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }\n}\n\ndependencies {\n    // Check the latest SNAPSHOT version from the link above\n    classpath 'com.google.android.horologist:horologist-tiles:XXXX.compose-YYYYY-SNAPSHOT'\n}\n

These builds are updated regularly, but there's no guarantee that we will create one for a given snapshot number.

Note: you might also see versions in the scheme x.x.x.ui-YYYY-SNAPSHOT. These are the same, just using an older suffix.

"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 1c0f31e09a..021738dec2 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,157 +2,157 @@ https://google.github.io/horologist/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/audio-ui/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/audio/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/auth-composables/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/auth-data-phone/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/auth-data/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/auth-googlesignin-guide/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/auth-overview/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/auth-sample-apps/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/auth-tokenshare-guide/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/auth-ui/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/composables/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/compose-layout/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/compose-material/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/compose-tools/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/contributing/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/datalayer-helpers-guide/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/datalayer/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/media-data/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/media-playerscreen/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/media-sample/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/media-toolkit/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/media-ui/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/media/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/media3-backend/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/network-awareness/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/simple-media-app-guide/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/tiles/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/updating-old/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/updating/ - 2023-12-04 + 2023-12-05 daily https://google.github.io/horologist/using-snapshot-version/ - 2023-12-04 + 2023-12-05 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 4b7a81bc10..15c436703a 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ