When you buy a watch you want it to, well, tell the time. So most Wear OS by Google watches include an always-on screen—no tapping, twisting or shaking required to see what time it is. With Wear OS 5.1 or higher, we have expanded this option to apps so that they can stay visible as long as you need them instead of disappearing when you drop your arm. Apps that supports this functionality are called always-on apps. There are numerous use cases that can benefit from being an always-on app, from shopping lists to exercise tracking.
This codelab will introduce you to the key concepts behind always-on apps. It will then walk you through converting an existing stopwatch app into an always-on app which will go on for as long as the user needs it . There is also a bonus section on increasing the refresh rate of the app while it is in ambient mode from once per minute to once every 10 seconds.
To start off let's learn a little bit about Wear OS and the key concepts behind always-on apps.
Wear OS is a wearable platform designed for small, powerful devices worn on the body. It is designed to deliver useful information when you need it most, intelligent answers to spoken questions, and tools to help reach fitness goals.
Some of these interaction can be brief, for example checking the location of your next meeting. Others can be longer, such as referring to your grocery list while shopping. During such longer interactions the default screen timeout can pause the application in the middle of the activity, resulting in friction in the user experience.. There are two potential solutions to this issue:
Similar to watch faces, an always on app has two modes and is able to switch between them in a session:
With the basic concepts out of the way, let's get started! To make this as quick as possible, we have prepared a sample project for you to build on. It contains some basic code and application settings necessary for building watch faces.
You can either download all the sample code to your computer...
...or clone the GitHub repository from the command line.
$ git clone https://github.com/googlesamples/io2015-codelabs.git
Start Android Studio, and select "Open an existing Android Studio project" from the Welcome screen, open the project directory, navigate to the directory wear/always-on
and open the build.gradle
file in that directory:
If the following screen appears, click OK on "Import Project from Gradle" screen without making any changes.
In the upper left hand corner of the project window, you should see something like this:
There are three folder icons. Each of them are known as a "module". Please note that Android Studio might take several seconds to compile the project in the background for the first time. During this time, you will see a spinner in the status bar at the bottom of Android Studio:
We recommend that you wait until this has finished before making code changes. This will allow Android Studio to pull in all the necessary components. In addition, if you get a prompt saying "Reload for language changes to take effect?" or something similar, select "Yes".
If you need help setting up an Wear OS emulator, please refer to the "Launch the emulator and run your Wear OS app" section of the "Creating and Running a Wearable App" article.
Let's run it on a watch / emulator.
Waiting for device. Target device: Android_Wear_Round_API_22 [emulator-5554] Uploading file local path: /Users/hellouser/AndroidStudioProject/android-codelab-always-on/1-base/build/outputs/apk/1-base-debug.apk remote path: /data/local/tmp/com.android.example.ambientstopwatch Installing com.android.example.ambientstopwatch DEVICE SHELL COMMAND: pm install -r "/data/local/tmp/com.android.example.ambientstopwatch" pkg: /data/local/tmp/com.android.example.ambientstopwatch Success Launching application: com.android.example.ambientstopwatch/com.android.example.alwaysonstopwatch.StopwatchActivity. DEVICE SHELL COMMAND: am start -n "com.android.example.ambientstopwatch/com.android.example.alwaysonstopwatch.StopwatchActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.android.example.ambientstopwatch/com.android.example.alwaysonstopwatch.StopwatchActivity }
Here's what it should look like. Don't worry if the power button to the right do not appear in the emulator - this is okay!
All right, you're set up and ready to start converting the application into an always-on app. We'll set off using the 1-base
module, which is the starting point for the stopwatch that we'll be building upon. You will be adding code from each step to 1-base
.
Each of the following modules can be used as reference points to check your work or for reference if you encounter any issues.
Overview of key components
StopwatchActivity
- This is our stop watch. - This file is located in the directory 1-base/java/com/android/example/alwaysonstopwatch
. In Android Studio, this is located under 1-base/java/com.android.example.alwaysonstopwatch
. Within this Activity class, we have:onCreate
- In this method, we wire up the UI elements (buttons, text views, etc). It also contains code which attaches the buttons to actions when they are clicked.mActiveModeUpdateHandler
variable and UpdateHandler
class - These two work together to power the stopwatch by updating the screen once a second when the stopwatch is in action.mActiveClockUpdateHandler
variable and UpdateClockHandler
class - These two work together to power the current time (updates once a minute). While the stopwatch is the main use-case for our app, users will still want to know the time if they are timing something that could last many minutes to hours.toggleStartStop
- This controls the behaviour when the stopwatch is started or paused..res/layout/activity_stopwatch.xml
- This is the layout of the stop watch UI.directory where the screen layout is stored.In this step you've learned about:
Let's start making this app an always-on app!
In this step, we will start making our app an always-on app. It will have the ability to jump between active and ambient mode and it will stay in the foreground until the user explicitly dismisses it. The screen will update once a minute which is the default for always-on apps.
In order to reference the new functionalities related to always-on apps, we need to add one dependencies to the build.gradle
file for com.google.android.wearable:wearable:1.0.0
:
dependencies {
compile 'com.google.android.support:wearable:1.2.0'
// Add the following line
provided 'com.google.android.wearable:wearable:1.0.0'
}
To convert our app to an always-on app, we need to change several attributes related to our app in AndroidManifest.xml
. This is located here:
We need to make the following changes to the file:
manifest
and uses-feature
elements:<uses-permission android:name="android.permission.WAKE_LOCK" />
application
and activity
elements:<uses-library android:name="com.google.android.wearable" android:required="true" />
StopwatchActivity.java
which keeps the screen on all the time:getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
Supporting ambient mode and making our app an always-on app requires additional lifecycle states for the activity. These additional states are built into a new activity class called WearableActivity
. To support ambient mode, we need to perform the following steps:
StopwatchActivity
from extending Activity
to extending WearableActivity:
public class StopwatchActivity extends WearableActivity {
setAmbientEnabled():
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stop_watch);
setAmbientEnabled();
// ...
If you run the application now, it will stay in the foreground but it does not understand what it should do differently in interactive mode and ambient mode. As a result, in ambient mode, it will be stuck at whatever time that it gets to and there will be no update until you wake it up into interactive mode.
Before we begin changing the lifecycle code, we will need to: 1) add a notice to remind the user that the screen is updated once a minute (this is the default for always-on apps and you can find out how you can change it in the next section) and 2) load some of the settings including the colours that we use for when we are in interactive mode so we can wake up to them from a black and white ambient mode:
string.xml
under res/values
, add the following:<string name="updatefrequencynotice">Updates every minute</string>
activity_stop_watch.xml
under res/layout
, add the following after the last button control within the GridLayout
element:<TextView android:id="@+id/notice"
android:layout_columnSpan="2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/updatefrequencynotice"
android:visibility="invisible"
android:textColor="@color/white"
android:includeFontPadding="false"
android:textSize="12sp" />
StopwatchActivity
TextView
variable called mNotice
.GridLayout
variable call mBackground
private int mActiveBackgroundColor;
private int mActiveForegroundColor;
mNotice
in onCreate
and set anti-aliasing to false as this will take less processing power to render:mNotice = (TextView) findViewById(R.id.notice);
mNotice.getPaint().setAntiAlias(false);
mBackground
in onCreate:
mBackground = (GridLayout) findViewById(R.id.grid_background);
color.xml
setting file by adding the following lines into the onCreate
method in StopwatchActivity
:mActiveBackgroundColor = getResources().getColor(R.color.activeBackground);
mActiveForegroundColor = getResources().getColor(R.color.activeText);
Another thing we should do is to stop the handler from running in ambient mode, in the method updateDisplayAndSetRefresh
we should add a check to see if the watch is in ambient mode before we schedule a delay call to the handler. Developers can check whether the app is currently in ambient mode by calling isAmbient()
- wrap this code in a check like so
//...
if(!isAmbient()) {
long timeMs = System.currentTimeMillis();
long delayMs = ACTIVE_INTERVAL_MS -
(timeMs % ACTIVE_INTERVAL_MS);
Log.d(TAG, "NOT ambient - delaying by: " + delayMs);
mActiveModeUpdateHandler
.sendEmptyMessageDelayed(MSG_UPDATE_SCREEN, delayMs);
}
//...
When the watch is in ambient mode (and in deep sleep), there is a callback for updates, so you do not need the handler. In addition, a handler can not wake up the processor from a sleep state, so it won't work anyway. We will expand on this in the next section.
To tell the system what to do in ambient mode, we override three methods
onEnterAmbient(...)
This is called when the application goes from interactive to ambient mode. This is where we typically change the formatting of the screen elements. Feel free to Copy & Paste the following code into the StopwatchActivity to account for ambient mode.
@Override
public void onEnterAmbient(Bundle ambientDetails) {
Log.d(TAG, "ENTER Ambient");
super.onEnterAmbient(ambientDetails);
if (mRunning) {
mActiveModeUpdateHandler.removeMessages(R.id.msg_update);
mNotice.setVisibility(View.VISIBLE);
}
mActiveClockUpdateHandler.removeMessages(R.id.msg_update);
mTimeView.setTextColor(Color.WHITE);
Paint textPaint = mTimeView.getPaint();
textPaint.setAntiAlias(false);
textPaint.setStyle(Paint.Style.STROKE);
textPaint.setStrokeWidth(2);
mStartStopButton.setVisibility(View.INVISIBLE);
mResetButton.setVisibility(View.INVISIBLE);
mBackground.setBackgroundColor(Color.BLACK);
mClockView.setTextColor(Color.WHITE);
mClockView.getPaint().setAntiAlias(false);
updateClock();
}
onExitAmbient()
Basically we invert what we have done before within onEnterAmbient(...)
@Override
public void onExitAmbient() {
Log.d(TAG, "EXIT Ambient");
super.onExitAmbient();
mTimeView.setTextColor(mActiveForegroundColor);
Paint textPaint = mTimeView.getPaint();
textPaint.setAntiAlias(true);
textPaint.setStyle(Paint.Style.FILL);
mStartStopButton.setVisibility(View.VISIBLE);
mResetButton.setVisibility(View.VISIBLE);
mBackground.setBackgroundColor(mActiveBackgroundColor);
mClockView.setTextColor(mActiveForegroundColor);
mClockView.getPaint().setAntiAlias(true);
mActiveClockUpdateHandler.sendEmptyMessage(R.id.msg_update);
if (mRunning) {
mNotice.setVisibility(View.INVISIBLE);
updateDisplayAndSetRefresh();
}
}
onUpdateAmbient()
This method is being called once a minute while in ambient mode by the framework and developers should tell Wear OS what to update by:
super.onUpdateAmbient();
updateClock()
updateDisplayAndSetRefresh()
If you run the app now, it should show the something similar to following in interactive mode - it's not all that different to before:
However, if you wait for a timeout on a real device (for emulator press F7 or fn + F7) to switch to ambient mode, you will see the following and this will update once a minute:
If you see the error INSTALL_FAILED_MISSING_SHARED_LIBRARY
, try updating the Wear emulator to version 22 or later. The ambient APIs are only available from this version.
In this step you've learned about:
An optional activity to learn about increasing the update frequency of the app to more than once a minute while in ambient mode. Most apps won't need to update faster than once per minute but this maybe appropriate, for example, for a running app showing the user the current pace.
If you still have time but don't fancy having a go at increasing the refresh rate, we encourage you to alter the different parameters of the screen elements, for example, layout, stroke size, color of the various screen element, etc. Let's see what you get!
For some applications, developers may want to update more frequently than once per minute. Since the Wear OS device is in deep sleep in ambient mode, the Handler
class will not run in this state. As a result, we will need to setup an AlarmManager
which will wake up the application at set intervals and update the screen.
The first step is to set up the AlarmManager
variable and a PendingIntent
which will tell Android which Activity
to launch when the AlarmManager
fires. To do this, we will make the following changes in StopwatchActivity
:
AlarmManager
named mAmbientStateAlarmManager
PendingIntent
named mAmbientStatePendingIntent
AMBIENT_INTERVAL_MS
) of type long for how often the screen will be updated. In our case, we will set this to TimeUnit.SECONDS.toMillis(10)
or 10 seconds.mAmbientStateAlarmManager
, we set it to (AlarmManager) getSystemService(Context.ALARM_SERVICE);
mAmbientStatePendingIntent
,Intent
which points at the StopwatchActivity.class:Intent intent = new Intent(getApplicationContext(),
StopwatchActivity.class);
mAmbientStatePendingIntent
using the following. mAmbientStatePendingIntent = PendingIntent.getActivity(
getApplicationContext(),
MSG_UPDATE_SCREEN,
intent,
PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager
requires a permission to run. Open AndroidManifest.xml
and put the following in just below the WAKE_LOCK
permission we added in the last step:<uses-permission
android:name="com.android.alarm.permission.SET_ALARM"/>
string.xml
under res/values
,<string name="updatefrequencynotice">Updates every 10 seconds</string>
With the basics in place, it's now time to add the code to trigger the AlarmManager. We need to update the code where previously we would have had the Handler code updating the screen. So this will be in the updateDisplayAndSetRefresh
method:
if (!isAmbient()) {
// same code as before...
} else {
// Our new code for AlarmManager goes here!
}
else
part which is when the watch is in ambient mode), we calculate how long we should wait until the next alarm using System.currentTimeMillis()
:long timeMs = System.currentTimeMillis();
long delayMs = AMBIENT_INTERVAL_MS - (timeMs % AMBIENT_INTERVAL_MS);
long triggerTimeMs = timeMs + delayMs;
cancel
method on mAmbientStateAlarmManager
feeding in the mAmbientStatePendingIntent
. mAmbientStateAlarmManager.setExact(
AlarmManager.RTC_WAKEUP,
triggerTimeMs,
mAmbientStatePendingIntent);
onExitAmbient
mode, check if the stopwatch is running just below super.onExitAmbient()
. If it is, cancel any alarm using the cancel
method on mAmbientStateAlarmManager
. Don't worry about starting the Handler
related code which will update the screen once a second in interactive mode. This is handled by updateDisplayAndSetRefresh
when it is called further down in the method.onDestroy
method when the application exitsonNewIntent
, this will be called when the alarm is fired:@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
updateDisplayAndSetRefresh();
}
If you run the application again, it should update every 10 seconds:
In this step you've learned about:
AlarmManager
to schedule these updates even when the device is in deep sleep in Ambient mode