Wear Os Daily Face Changer

 

This guide will walk you through the key components, using modern Android development practices with Jetpack Compose for Wear OS and StateFlow for reactive data handling.

Project Goal

We will build a watch face that has a collection of backgrounds. Each day, at midnight, the watch face will automatically cycle to the next background in the sequence, creating a fresh look.

Prerequisites

 * Android Studio (latest version).

 * A Wear OS emulator or a physical Wear OS device for testing.

 * Basic knowledge of Kotlin.

Step 1: Project Setup

 * Open Android Studio.

 * Click New Project.

 * Select the Wear OS tab.

 * Choose Watch Face.

 * Click Next.

 * Configure your project:

   * Name: Daily Changer Watch Face

   * Package name: com.example.dailychanger

   * Language: Kotlin

   * Build configuration: Groovy DSL or KTS

   * Minimum SDK: API 28 or higher is recommended.

 * Click Finish. Android Studio will generate a template watch face project.

Step 2: Define the Daily Watch Faces

First, let's define the "faces" we want to cycle through. For simplicity, our "faces" will just be different background images.

 * Add a few sample background images to your res/drawable folder. Let's call them bg_day1.png, bg_day2.png, bg_day3.png, etc.

 * Create a new Kotlin file named WatchFaceRepo.kt. We'll define our available faces here using an enum for type safety and easy management.

<!-- end list -->

// file: WatchFaceRepo.kt

package com.example.dailychanger


import androidx.annotation.DrawableRes


enum class DailyBackground(

    @DrawableRes val backgroundRes: Int

) {

    FOREST(R.drawable.bg_forest), // Make sure you have these drawables

    OCEAN(R.drawable.bg_ocean),

    MOUNTAIN(R.drawable.bg_mountain),

    CITY(R.drawable.bg_city);


    companion object {

        fun fromIndex(index: Int): DailyBackground {

            // Cycle through the enum values

            return values()[index % values().size]

        }

    }

}


Replace the drawable names with the ones you added.

Step 3: Create the Watch Face Composable UI

We'll use Compose for Wear OS to draw our watch face. The generated project already has a basic structure. Let's modify it.

Open the file named WatchFace.kt (or similar, from the template) and replace its content with this:

// file: WatchFace.kt

package com.example.dailychanger


import androidx.compose.foundation.Image

import androidx.compose.foundation.layout.Box

import androidx.compose.foundation.layout.fillMaxSize

import androidx.compose.runtime.Composable

import androidx.compose.runtime.remember

import androidx.compose.ui.Alignment

import androidx.compose.ui.Modifier

import androidx.compose.ui.layout.ContentScale

import androidx.compose.ui.res.painterResource

import androidx.compose.ui.tooling.preview.Devices

import androidx.compose.ui.tooling.preview.Preview

import androidx.wear.compose.material.Text

import java.time.LocalTime

import java.time.format.DateTimeFormatter


@Composable

fun DailyWatchFace(

    background: DailyBackground,

    time: LocalTime

) {

    val timeFormatter = remember { DateTimeFormatter.ofPattern("HH:mm") }


    Box(

        modifier = Modifier.fillMaxSize(),

        contentAlignment = Alignment.Center

    ) {

        // 1. The daily changing background

        Image(

            painter = painterResource(id = background.backgroundRes),

            contentDescription = "watch background",

            modifier = Modifier.fillMaxSize(),

            contentScale = ContentScale.Crop

        )


        // 2. The time display

        Text(

            text = time.format(timeFormatter)

        )

    }

}


@Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)

@Composable

fun DailyWatchFacePreview() {

    DailyWatchFace(

        background = DailyBackground.FOREST,

        time = LocalTime.now()

    )

}


Step 4: Manage State and Daily Logic

This is the core of our application. We need a way to:

 * Check the current date.

 * Decide which face to show.

 * Save this choice so it persists.

 * Update when the day changes.

We will use SharedPreferences for simple data storage. Create a new file DailyFaceStateHolder.kt.

// file: DailyFaceStateHolder.kt

package com.example.dailychanger


import android.content.Context

import kotlinx.coroutines.flow.MutableStateFlow

import kotlinx.coroutines.flow.StateFlow

import kotlinx.coroutines.flow.asStateFlow

import java.time.LocalDate


object DailyFaceStateHolder {


    private const val PREFS_NAME = "DailyWatchFacePrefs"

    private const val KEY_LAST_UPDATE_DATE = "last_update_date"

    private const val KEY_CURRENT_FACE_INDEX = "current_face_index"


    // Use StateFlow to notify the watch face of changes reactively

    private val _currentBackground = MutableStateFlow(DailyBackground.FOREST)

    val currentBackground: StateFlow<DailyBackground> = _currentBackground.asStateFlow()


    fun updateFaceIfNeeded(context: Context) {

        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)

        val today = LocalDate.now().toString()

        val lastUpdateDate = prefs.getString(KEY_LAST_UPDATE_DATE, null)


        // Only update if the date has changed

        if (today != lastUpdateDate) {

            val lastIndex = prefs.getInt(KEY_CURRENT_FACE_INDEX, -1)

            val nextIndex = lastIndex + 1


            val newFace = DailyBackground.fromIndex(nextIndex)

            _currentBackground.value = newFace


            // Save the new state

            with(prefs.edit()) {

                putString(KEY_LAST_UPDATE_DATE, today)

                putInt(KEY_CURRENT_FACE_INDEX, nextIndex)

                apply()

            }

        } else {

            // On boot or first run, just load the saved face

            val savedIndex = prefs.getInt(KEY_CURRENT_FACE_INDEX, 0)

            _currentBackground.value = DailyBackground.fromIndex(savedIndex)

        }

    }

}


Step 5: Implement the Daily Update Trigger

To change the face at midnight, we need a mechanism that runs reliably. The best tool for this is AlarmManager combined with a BroadcastReceiver.

 * Create the BroadcastReceiver:

Create a new file DailyUpdateReceiver.kt. This receiver will be triggered by our alarm and will simply call our state update logic.

// file: DailyUpdateReceiver.kt

package com.example.dailychanger


import android.content.BroadcastReceiver

import android.content.Context

import android.content.Intent


class DailyUpdateReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent?) {

        // When the alarm fires, trigger the face update logic

        DailyFaceStateHolder.updateFaceIfNeeded(context)

    }

}


 * Schedule the Alarm:

Create a helper object to manage scheduling. Create AlarmScheduler.kt.

// file: AlarmScheduler.kt

package com.example.dailychanger


import android.app.AlarmManager

import android.app.PendingIntent

import android.content.Context

import android.content.Intent

import java.util.Calendar


object AlarmScheduler {


    fun scheduleDailyUpdate(context: Context) {

        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager


        val intent = Intent(context, DailyUpdateReceiver::class.java)

        val pendingIntent = PendingIntent.getBroadcast(

            context,

            0,

            intent,

            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

        )


        // Set the alarm to start at approximately midnight

        val calendar = Calendar.getInstance().apply {

            timeInMillis = System.currentTimeMillis()

            set(Calendar.HOUR_OF_DAY, 0)

            set(Calendar.MINUTE, 1) // A minute past midnight to be safe

        }


        // Use inexact repeating to save battery. The system will fire it around midnight.

        alarmManager.setInexactRepeating(

            AlarmManager.RTC_WAKEUP,

            calendar.timeInMillis,

            AlarmManager.INTERVAL_DAY,

            pendingIntent

        )

    }

}


Step 6: Update the Watch Face Service

Now, let's tie everything together in our main service file (DailyChangerWatchFaceService.kt or similar). This service will:

 * Listen to the StateFlow from our DailyFaceStateHolder.

 * Schedule the daily alarm when it's created.

 * Render the Composable UI.

Replace the content of your WatchFaceService file:

// file: DailyChangerWatchFaceService.kt

package com.example.dailychanger


import android.view.SurfaceHolder

import androidx.compose.runtime.Composable

import androidx.compose.runtime.collectAsState

import androidx.compose.runtime.getValue

import androidx.wear.watchface.ComplicationSlotsManager

import androidx.wear.watchface.WatchFace

import androidx.wear.watchface.WatchFaceService

import androidx.wear.watchface.WatchFaceType

import androidx.wear.watchface.WatchState

import androidx.wear.watchface.style.CurrentUserStyleRepository

import kotlinx.coroutines.flow.StateFlow

import java.time.LocalTime


class DailyChangerWatchFaceService : WatchFaceService() {


    override fun onCreate() {

        super.onCreate()

        // Check the face state on service creation (e.g., after boot or update)

        DailyFaceStateHolder.updateFaceIfNeeded(applicationContext)

        // Schedule the alarm that will trigger future daily updates

        AlarmScheduler.scheduleDailyUpdate(applicationContext)

    }


    override suspend fun createWatchFace(

        surfaceHolder: SurfaceHolder,

        watchState: WatchState,

        complicationSlotsManager: ComplicationSlotsManager,

        currentUserStyleRepository: CurrentUserStyleRepository

    ): WatchFace {

        return WatchFace(

            watchFaceType = WatchFaceType.DIGITAL,

            renderer = WatchFaceRenderer(

                surfaceHolder = surfaceHolder,

                watchState = watchState,

                currentUserStyleRepository = currentUserStyleRepository,

                // Pass our reactive state flow to the renderer

                backgroundStateFlow = DailyFaceStateHolder.currentBackground

            )

        )

    }

}


// The renderer now takes the StateFlow

private class WatchFaceRenderer(

    surfaceHolder: SurfaceHolder,

    watchState: WatchState,

    currentUserStyleRepository: CurrentUserStyleRepository,

    private val backgroundStateFlow: StateFlow<DailyBackground>

) : WatchFaceRenderer(

    surfaceHolder,

    currentUserStyleRepository,

    watchState

) {

    override suspend fun getComplicationState(complicationId: Int) = null


    @Composable

    override fun Build() {

        // Collect the state flow as compose state.

        // When the flow emits a new value, this composable will re-render.

        val background by backgroundStateFlow.collectAsState()


        // We check the state here as well in case the watch face becomes visible

        // after being dormant for a day.

        DailyFaceStateHolder.updateFaceIfNeeded(context)


        DailyWatchFace(

            background = background,

            time = LocalTime.now()

        )

    }

}


Note: Depending on your template, you might need to adjust the renderer class structure. The key is to pass DailyFaceStateHolder.currentBackground to it and use collectAsState().

Step 7: Update AndroidManifest.xml

Finally, we must declare our receiver and the necessary permissions in the manifest.

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android">


    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />



    <application

        android:allowBackup="true"

        android:icon="@mipmap/ic_launcher"

        android:label="@string/app_name"

        android:supportsRtl="true"

        android:theme="@android:style/Theme.DeviceDefault">


        <service

            android:name=".DailyChangerWatchFaceService"

            android:exported="true"

            android:label="@string/watch_face_name"

            android:permission="android.permission.BIND_WALLPAPER">

            </service>


        <receiver

            android:name=".DailyUpdateReceiver"

            android:enabled="true"

            android:exported="false">

            <intent-filter>

                 <action android:name="android.intent.action.BOOT_COMPLETED" />

            </intent-filter>

        </receiver>


    </application>

</manifest>


How to Run

 * Connect your Wear OS device or start an emulator.

 * Run the application from Android Studio.

 * On your watch, long-press the current watch face to open the watch face selector.

 * Find and select your "Daily Changer Watch Face".

It will now show the first face. To test the daily change logic without waiting, you can either change the date on your test device or add a temporary button somewhere to call DailyFaceStateHolder.updateFaceIfNeeded(context). The watch face will update automatically every day around midnight.


Comments

Popular posts from this blog

Kotlin Math Operations and Functions Overview

Kotlin Strings: Features and Operations Guide

Kotlin Android Program (QCR) Application Codes That Read Text in Photos