We assume you already possess the basic knowledge of how to create a watch face. If not, you would be strongly recommended to browse through the previous tutorial, “Programming Android Wear Watch Faces.” Being able to program your own watch faces is great, but when it comes to making them appeal to your users, you would defnitely need to consider providing many more configuration options from both the mobile and wearable sides. This tutorial will focus on how to prepare these configuration activities and how to channel your user’s selected results to the wearable listener service through the Wearable Data Layer API offered as part of Google Play Service.
Overview and Setup in Mobile and Wearable Manifest Files
We can support optional configurations both on the wearable device as well as on the mobile companion device. That means it involves declaring activities and their intent filters in the respective manifest files. And, of course, both configuration activity codes need to be created. To use Google’s Wearable Data Layer API as part of Google Play Service, the metadata entry for com.google.android.gms.version is required. Finally, we will then add the metadata entries for the configuration activities to the service declaration in the wearable manifest file.
We are going to extend one of the examples, CLiu Digital, from the previous watch face tutorial and try to skip the duplicate portion by highlighting the additional info. Here is updated info for the wearable AndroidManifest.xml manifest file with our new configuration activity WatchFaceCLiuWearConfigActivity. Note the metadata value must be consistent with the activity action name as in com.example.nobody.watchfacecliu.CONFIG_CLIU.
<service android_name=".WatchFaceCLiuService" android_label="CLiu" android_allowEmbedded="true" android_taskAffinity="" android_permission="android.permission.BIND_WALLPAPER" > ... <meta-data android_name="com.google.android.wearable.watchface. companionConfigurationAction" android_value="com.example.nobody.watchfacecliu.CONFIG_CLIU" /> <meta-data android_name="com.google.android.wearable.watchface. wearableConfigurationAction" android_value="com.example.nobody.watchfacecliu.CONFIG_CLIU" /> </service> <activity android_name=".WatchFaceCLiuWearConfigActivity" android_label="CLiu Wear"> <intent-filter> <action android_name="com.example.nobody.watchfacecliu. CONFIG_CLIU" /> <category android_name="com.google.android.wearable.watchface. category.WEARABLE_CONFIGURATION" /> <category android_name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <meta-data android_name="com.google.android.gms.version" android_value="@integer/google_play_services_version" />
And here is the updated manifest file for the mobile AndroidManifest.xml with our new configuration activity, WatchFaceCLiuMobileConfigActivity.
<activity android_name=".WatchFaceCLiuMobileConfigActivity" android_label="CLiu Mobile"> <intent-filter> <action android_name="com.example.nobody. watchfacecliu.CONFIG_CLIU" /> <category android_name="com.google.android.wearable. watchface.category.COMPANION_CONFIGURATION" /> <category android_name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <meta-data android_name="com.google.android.gms.version" android_value="@integer/google_play_services_version" />
Again, as in the previous tutorial, the credits of all the excellent photography as illustrated in Figure 1, Figure 2, and Figure 3 should go to my Australian friend, Chris Blunt, for the usage permission. You can find his link in the References section.
Figure 1: CLiu – 1
Figure 2: CLiu – 2
Figure 3: CLiu – 3
Creating a Wearable Listener Service
The listener integrated in the main watch face service is built to check for the data sent from configuration activities through the Wearable Data Layer API. GoogleApiClient is provided for this purpose to establish the connection for sending and receiving user data. DataApi.DataListener will be notified whenever things are changed in the data layer. ResultCallback is called whenever GoogleApiClient connects.
The most important piece of code in the listener is implemented in updateParamsForDataItem, which checks for the unique path and key/value pairs in the incoming DataItem of the data layer. Note the unique path must start with a “/” as in our example, “/watch_face_config_cliu”.
public class WatchFaceCLiuService extends CanvasWatchFaceService { private static final long UPDATE_INTERVAL = TimeUnit.SECONDS.toMillis(1); @Override public Engine onCreateEngine() { return new Engine(); } private class Engine extends CanvasWatchFaceService.Engine implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { ... private GoogleApiClient mGoogleApiClient; private int mTextColor = 0xffffffff; private float offsetx = (float)(-50 + 100 * Math.random()); private float offsety = (float)(-50 + 100 * Math.random()); @Override public void onCreate(SurfaceHolder holder) { super.onCreate(holder); ... mGoogleApiClient = new GoogleApiClient.Builder(WatchFaceCLiuService.this) .addApi(Wearable.API) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); } ... @Override public void onDraw(Canvas canvas, Rect bounds) { ... canvas.drawText(ts1, tx1, ty1, mDigitalPaint); ... } private void releaseGoogleApiClient() { if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) { Wearable.DataApi.removeListener(mGoogleApiClient, onDataChangedListener); mGoogleApiClient.disconnect(); } } @Override public void onConnected(Bundle bundle) { Wearable.DataApi.addListener(mGoogleApiClient, onDataChangedListener); Wearable.DataApi.getDataItems(mGoogleApiClient). setResultCallback(onConnectedResultCallback); } private void updateParamsForDataItem(DataItem item) { if ((item.getUri().getPath()).equals("/watch_face_config_cliu")) { DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap(); if (dataMap.containsKey("text_color")) { int tc = dataMap.getInt("text_color"); mDigitalPaint.setColor(tc); invalidate(); } } } private final DataApi.DataListener onDataChangedListener = new DataApi.DataListener() { @Override public void onDataChanged(DataEventBuffer dataEvents) { for (DataEvent event : dataEvents) { if (event.getType() == DataEvent.TYPE_CHANGED) { DataItem item = event.getDataItem(); updateParamsForDataItem(item); } } dataEvents.release(); if (isVisible() && !isInAmbientMode()) { invalidate(); } } }; private final ResultCallback<DataItemBuffer> onConnectedResultCallback = new ResultCallback<DataItemBuffer>() { @Override public void onResult(DataItemBuffer dataItems) { for (DataItem item : dataItems) { updateParamsForDataItem(item); } dataItems.release(); if (isVisible() && !isInAmbientMode()) { invalidate(); } } }; @Override public void onConnectionSuspended(int i) { } @Override public void onConnectionFailed(ConnectionResult connectionResult) { } @Override public void onDestroy() { mUpdateTimeHandler.removeMessages(MESSAGE_ID_UPDATE_TIME); releaseGoogleApiClient(); super.onDestroy(); } } }
Creating a Mobile Configuration Activity
Now we have the listening service ready, so we can start creating a configuration activity on the mobile companion side. As an example, we are providing the option to select the text color for the time display. Similarly, GoogleApiClient is initiated for the Wearable Data Layer API communication. The main code for sending data is implemented in sendParamsAndFinish. PutDataMapRequest is the data constructor for the unqiue path and key/value pairs. Wearable.DataApi.putDataItem is used to ship the structured data. In Figure 4, you should notice a small gear icon on top of the watch face preview. That means this watch face comes with a mobile configuration activity shown in Figure 5.
public class WatchFaceCLiuMobileConfigActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { private GoogleApiClient mGoogleApiClient; private int mTextColor = 0xffffffff; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.config); mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(Wearable.API) .build(); Button buttonOK = (Button)findViewById(R.id.buttonOK); buttonOK.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { RadioGroup radioTextColor = (RadioGroup)findViewById(R.id.radioTextColor); int selectedId = radioTextColor.getCheckedRadioButtonId(); switch (selectedId) { default: case R.id.radio_tc1: mTextColor = 0xffffffff; break; ... } sendParamsAndFinish(); } }); Button buttonCancel = (Button)findViewById(R.id.buttonCancel); buttonCancel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendParamsAndFinish(); } }); } // sends data through Google API private void sendParamsAndFinish() { PutDataMapRequest putDataMapReq = PutDataMapRequest.create("/watch_face_config_cliu"); putDataMapReq.getDataMap().putInt("text_color", mTextColor); PutDataRequest putDataReq = putDataMapReq.asPutDataRequest(); Wearable.DataApi.putDataItem(mGoogleApiClient, putDataReq); finish(); } @Override protected void onStart() { super.onStart(); mGoogleApiClient.connect(); } @Override protected void onStop() { if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) { mGoogleApiClient.disconnect(); } super.onStop(); } @Override public void onConnected(Bundle bundle) { } @Override public void onConnectionSuspended(int i) { } @Override public void onConnectionFailed(ConnectionResult connectionResult) { } }
Figure 4: CLiu Mobile
Figure 5: CLiu Mobile Config
Creating a Wearable Configuration Activity
Again, the configuration activity for the wearable device is very similar to the one on the mobile side. GoogleApiClient is initiated for the Wearable Data Layer API communication and data sending is done in sendParamsAndFinish. We will utilize the nicely developed color picker portion with animation in one of the Android Wear sample projects that you can import directly from “Android Studio’s Quick Start” menu and select “Import an Android code sample.” The layout of picking a color is through some visual color items arranged in WearableListView. In Figure 6, you should notice a small gear icon right below the watch face preview. That means this watch face comes with a wearable configuration activity shown in Figure 7.
public class WatchFaceCLiuWearConfigActivity extends Activity implements WearableListView.ClickListener, WearableListView.OnScrollListener { private GoogleApiClient mGoogleApiClient; private int mTextColor = 0xffffffff; private TextView mHeader; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_config); mHeader = (TextView)findViewById(R.id.header); WearableListView listView = (WearableListView)findViewById(R.id.color_picker); ... String[] colors = getResources().getStringArray(R.array.color_array); listView.setAdapter(new ColorListAdapter(colors)); mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() { @Override public void onConnected(Bundle connectionHint) {} @Override public void onConnectionSuspended(int cause) {} }) .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() { @Override public void onConnectionFailed(ConnectionResult result) {} }) .addApi(Wearable.API) .build(); } @Override protected void onStart() { super.onStart(); mGoogleApiClient.connect(); } @Override protected void onStop() { if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) { mGoogleApiClient.disconnect(); } super.onStop(); } // sends data through Google API private void sendParamsAndFinish() { PutDataMapRequest putDataMapReq = PutDataMapRequest.create("/watch_face_config_cliu"); putDataMapReq.getDataMap().putInt("text_color", mTextColor); PutDataRequest putDataReq = putDataMapReq.asPutDataRequest(); Wearable.DataApi.putDataItem(mGoogleApiClient, putDataReq); finish(); } @Override // WearableListView.ClickListener public void onClick(WearableListView.ViewHolder viewHolder) { ColorItemViewHolder colorItemViewHolder = (ColorItemViewHolder)viewHolder; mTextColor = colorItemViewHolder.mColorItem.getColor(); sendParamsAndFinish(); } @Override // WearableListView.ClickListener public void onTopEmptyRegionClick() { } @Override // WearableListView.OnScrollListener public void onScroll(int scroll) { } @Override // WearableListView.OnScrollListener public void onAbsoluteScrollChange(int scroll) { float newTranslation = Math.min(-scroll, 0); mHeader.setTranslationY(newTranslation); } @Override // WearableListView.OnScrollListener public void onScrollStateChanged(int scrollState) { } @Override // WearableListView.OnScrollListener public void onCentralPositionChanged(int centralPosition) { } private static class ColorItem extends LinearLayout implements WearableListView.OnCenterProximityListener { ... } private class ColorListAdapter extends WearableListView.Adapter { ... } }
Figure 6: CLiu Wearable
Figure 7: CLiu Wearable Config
Conclusion
There is no better way of understanding the key ingredients of the watch face configurations than through studying examples. In addition to this tutorial, you should benefit a lot from several Android Wear sample projects available directly under “Import an Android code sample” in Android Studio.
References
- Download example source codes from the link at the bottom of this article
- Chris Blunt Photography at: https://www.facebook.com/Chris.Blunt.Photography
- Android Developers at: http://www.android.com/developers/
- Androidlet at http://www.androidlet.com
About the Author
Chunyen Liu has been a software professional in Taiwan and the United States. He is a published author of 40+ articles and 100+ tiny apps, software patentee, technical reviewer, and programming contest winner by ACM/IBM/SUN. He holds advanced degrees in Computer Science, trained in 20+ graduate-level courses. On the non-technical side, he is a certified coach, certified umpire, rated player of USA Table Tennis, and categorized medalist at State Championships and US Open.