MobileAndroidProviding Configurations in Android Wear

Providing Configurations in Android Wear

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

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.

TopCaw1
Figure 1: CLiu – 1

TopCaw2
Figure 2: CLiu – 2

TopCaw3
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) {
    }

}

TopCaw4
Figure 4: CLiu Mobile

TopCaw5
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 {
        ...
    }
}

TopCaw6
Figure 6: CLiu Wearable

TopCaw7
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

  1. Download example source codes from the link at the bottom of this article
  2. Chris Blunt Photography at: https://www.facebook.com/Chris.Blunt.Photography
  3. Android Developers at: http://www.android.com/developers/
  4. 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.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories