How to Create a Custom Ongoing Notification on Android Wear

Written by: on July 28

By now, you’ve probably heard of Android Wear.

If you’re lucky, you already have one of the first Wear watches, ready to begin developing an app using the new platform.

Google says if you currently have an app using NotificationCompat to send notifications, your app’s notifications will “just work” on Android Wear. But there is one caveat to this that is not explained in the documentation: ongoing notifications on your handheld device do not appear on your wearable device.

At Double Encore, we write a lot of sports apps where delivering real-time stats is the name of the game (no pun intended), but delivering a new notification every time a team scores unnecessarily interrupts the user. Instead, imagine an ongoing notification that the user can pull up that will always be up-to-date with latest game stats. Or, imagine an interactive notification that the user can touch after a critical score to favorite that play, and the app will remember that moment of the game so the user can go back and watch the recap later. Creating a custom ongoing notification on a smart watch engages the user far more than a standard notification without being distracting.

To achieve this, you need to create a separate wearable app to display your notification. You will then need to send a data event to your wearable app with the information needed to display your notification. The wearable app will then create the ongoing notification on the wearable device.

Check out this project to follow along with this example:

Create a new Wear project in Android Studio

Android Studio New Project dialog screenshot

This will create a project with two modules in it: mobile (the app that runs on the handheld) and wear (the app that runs on the wearable). The minimum SDK version for your handheld app can be whatever you want your app to support. The minimum SDK version for the wearable app must be 20.

Make sure you are running the latest version of Android Studio (0.8 or above) and latest SDK bits!

Android Studio will automatically include all the necessary support and Play Services libraries in your build.gradle files, but you still need to add the following metadata to AndroidManifest.xml in both the mobile and wear modules:

<meta-data android:name="com.google.android.gms.version"
    android:value="@integer/google_play_services_version"/>

 

Create an activity to display a custom notification in the wearable app

Chances are you’ll want to display a custom notification that better supports your brand rather than use one of the standard notification styles. If one of the standard notification styles is good enough for you, you can skip this step and use one of them in the next step.

First, let’s create an activity to display your custom notification. In this example, we’ll start with a layout that displays an ImageView and TextView, but you can make the view as complex as you want since Wear uses an activity to display your custom notification. No RemoteViews to wrestle here!

  1. Create a new activity in the wear module of your project and call it NotificationActivity. This activity is separate from your app’s default activity.
  2. Create a new layout for your activity and call it activity_notification.xml:
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <ImageView
            android:id="@+id/image_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        <TextView
            android:id="@+id/text_view"
            android:layout_gravity="center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </FrameLayout>
  3. The contents that populate the ImageView and TextView will be passed in as extras in the intent for your activity. Extract these elements in the activity’s onCreate() method:
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_notification);
    
        mImageView = (ImageView) findViewById(R.id.image_view);
        mTextView = (TextView) findViewById(R.id.text_view);
    
        Intent intent = getIntent();
        if (intent != null) {
            mTextView.setText(intent.getStringExtra(EXTRA_TITLE));
    
            final Asset asset = intent.getParcelableExtra(EXTRA_IMAGE);
    
            loadBitmapFromAsset(this, asset, mImageView);
        }
    }
  4. The image is passed as an Asset object, which we need to deserialize into a Bitmap. Here is the loadBitmapFromAsset() method that does this work on a background thread:
    public static void loadBitmapFromAsset(final Context context,
            final Asset asset, final ImageView target) {
        if (asset == null) {
            throw new IllegalArgumentException("Asset must be non-null");
        }
    
        new AsyncTask<Asset, Void, Bitmap>() {
            @Override
            protected Bitmap doInBackground(Asset... assets) {
                GoogleApiClient googleApiClient = new GoogleApiClient.Builder(context)
                        .addApi(Wearable.API)
                        .build();
                ConnectionResult result =
                        googleApiClient.blockingConnect(
                                1000, TimeUnit.MILLISECONDS);
                if (!result.isSuccess()) {
                    return null;
                }
    
                // convert asset into a file descriptor and block until it's ready
                InputStream assetInputStream = Wearable.DataApi.getFdForAsset(
                        googleApiClient, assets[0]).await().getInputStream();
                googleApiClient.disconnect();
    
                if (assetInputStream == null) {
                    Log.w(TAG, "Requested an unknown Asset.");
                    return null;
                }
    
                // decode the stream into a bitmap
                return BitmapFactory.decodeStream(assetInputStream);
            }
    
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                if (bitmap != null) {
                    target.setImageBitmap(bitmap);
                }
            }
        }.execute(asset);
    }
  5. Finally, add NotificationActivity to your wearable app’s manifest with the following attributes:
    <activity android:name=".NotificationActivity"
                android:exported="true"
                android:allowEmbedded="true"
                android:taskAffinity=""
                android:theme="@android:style/Theme.DeviceDefault.Light" />
    The attribute allowEmbedded is new for Wear.

Create a WearableListenerService to receive data events and display your notification on the wearable

Next, we will need to create a service in the wearable app to receive data events from the handheld app and build the custom ongoing notification on the wearable.

  1. Create a class named OngoingNotificationListenerService that extends WearableListenerService.
    WearableListenerService handles the GoogleApiClient callbacks behind the scenes
  2. Connect to GoogleApiClient in your service’s onCreate():
    @Override
    public void onCreate() {
        super.onCreate();
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addApi(Wearable.API)
                .build();
        mGoogleApiClient.connect();
    }
  3. Implement onDataChanged() to handle the data event:
    @Override
    public void onDataChanged(DataEventBuffer dataEvents) {
        final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents);
        dataEvents.close();
    
        if (!mGoogleApiClient.isConnected()) {
            ConnectionResult connectionResult = mGoogleApiClient
                    .blockingConnect(30, TimeUnit.SECONDS);
            if (!connectionResult.isSuccess()) {
                Log.e(TAG, "Service failed to connect to GoogleApiClient.");
                return;
            }
        }
    
        for (DataEvent event : events) {
            if (event.getType() == DataEvent.TYPE_CHANGED) {
                String path = event.getDataItem().getUri().getPath();
                if (PATH.equals(path)) {
                    // Get the data out of the event
                    DataMapItem dataMapItem =
                            DataMapItem.fromDataItem(event.getDataItem());
                    final String title = dataMapItem.getDataMap().getString(KEY_TITLE);
                    Asset asset = dataMapItem.getDataMap().getAsset(KEY_IMAGE);
    
                    // Build the intent to display our custom notification
                    Intent notificationIntent =
                            new Intent(this, NotificationActivity.class);
                    notificationIntent.putExtra(
                            NotificationActivity.EXTRA_TITLE, title);
                    notificationIntent.putExtra(
                            NotificationActivity.EXTRA_IMAGE, asset);
                    PendingIntent notificationPendingIntent = PendingIntent.getActivity(
                            this,
                            0,
                            notificationIntent,
                            PendingIntent.FLAG_UPDATE_CURRENT);
    
                    // Create the ongoing notification
                    Notification.Builder notificationBuilder =
                            new Notification.Builder(this)
                                .setSmallIcon(R.drawable.ic_launcher)
                                .setLargeIcon(BitmapFactory.decodeResource(
                                        getResources(), R.drawable.ic_launcher))
                                .setOngoing(true)
                                .extend(new Notification.WearableExtender()
                                        .setDisplayIntent(notificationPendingIntent));
    
                    // Build the notification and show it
                    NotificationManager notificationManager =
                            (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
                    notificationManager.notify(
                            NOTIFICATION_ID, notificationBuilder.build());
                } else {
                    Log.d(TAG, "Unrecognized path: " + path);
                }
            }
        }
    }
    The values of PATH, KEY_IMAGE, and KEY_TITLE are constants defined here and also within the handheld app in the next section (not shown) to identify the data request and data fields. Classes in the handheld app cannot be accessed from the wearable app, and vice versa, so you should create a shared library module that defines these constants.
  4. Add the service to your wearable app’s manifest with the following intent filter:
    <service android:name=".OngoingNotificationListenerService">
        <intent-filter>
            <action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
        </intent-filter>
    </service>

Send a message to the wearable app from the handheld app

In the activity in the handheld app that will be triggering the notification, you will need to create an instance of GoogleApiClient and connect to it.

  1. First, make sure the activity implements GoogleApiClient.ConnectionCallbacks and GoogleApiClient.OnConnectionFailedListener. Then, add the following to your activity’s lifecycle methods:
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addApi(Wearable.API)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .build();
    
        if (!mGoogleApiClient.isConnected()) {
            mGoogleApiClient.connect();
        }
    }
    
    @Override
    public void onDestroy() {
        if (mGoogleApiClient.isConnected()) {
            mGoogleApiClient.disconnect();
        }
    
        super.onDestroy();
    }
  2. Next we need to send a data request to the wearable. In our example activity, a button is hooked up to the following code when clicked to trigger this request:
        if (mGoogleApiClient.isConnected()) {
            PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(PATH);
                
            // Add data to the request
            putDataMapRequest.getDataMap().putString(KEY_TITLE,
                    String.format("hello world! %d", count++));
    
            Bitmap icon = BitmapFactory.decodeResource(
                    getResources(), R.drawable.ic_launcher);
            Asset asset = createAssetFromBitmap(icon);
            putDataMapRequest.getDataMap().putAsset(KEY_IMAGE, asset);
    
            PutDataRequest request = putDataMapRequest.asPutDataRequest();
    
            Wearable.DataApi.putDataItem(mGoogleApiClient, request)
                    .setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
                        @Override
                        public void onResult(DataApi.DataItemResult dataItemResult) {
                            Log.d(TAG, "putDataItem status: "
                                    + dataItemResult.getStatus().toString());
                        }
                    });
        }
    If the content of a new data request is not different from the previous request, the new request will get ignored. To ensure that each request gets delivered in this example, we make each request unique by incrementing the count value.
  3. Finally, implement createAssetFromBitmap() to convert a bitmap into a serializable Asset object:
    private static Asset createAssetFromBitmap(Bitmap bitmap) {
        final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream);
        return Asset.createFromBytes(byteStream.toByteArray());
    }
    Your handheld and wearable app MUST have the same package name. An app on the handheld cannot send messages to an app on the wearable with a different package name (and vice versa).

Pair and Deploy

Pair your wearable with your handheld and deploy the wearable app to the wearable and the mobile app to the handheld device. When your handheld app sends the data event, your custom ongoing notification will appear on your wearable.

To enable ADB debugging and pair your device for deployment, follow the procedures here and here.

Handheld app screenshot
The handheld app.
 
NotificationActivity Screenshot
Ongoing notification showing NotificationActivity.
 
Notification Mute action screenshot
Android provides a Mute action for ongoing notifications. Apps can be unmuted from the Android Wear app on the handheld.
 

Since we are creating an ongoing notification that is not dismissable, be sure to provide a way for the user to dismiss the notification. Otherwise the user will get annoyed and choose to mute your app, and never see a notification again! This can be done by sending a different message from the handheld app to the wearable app and letting OngoingNotificationListenerService cancel the notification it created. See the sample code to see how this can be done.

A two-way street

Your wearable app need not only act as a receiver for messages and data. By implementing an additional service in your handheld app that extends WearableListenerService, you can send messages from your wearable app’s NotificationActivity back to your handheld app for further action. This can be useful to launch a particular activity on the handheld device as the user interacts with the wearable notification.

There is quite a bit of untapped capability yet with this platform, as we begin to discover what Android Wear can do for users. Let us know what ideas you have to extend Android Wear beyond simply displaying your notifications!

References:

For more insights like this, please subscribe to our mailing list at the bottom of the page!

Carlos Hwa

Carlos Hwa is a mobile software engineer at Double Encore, specializing in Android Development. When not creating new solutions for Android Wear, Carlos enjoys rock climbing, volunteering for community organizations like Denver Urban Scholars and Colorado Youth at Risk, and eating other people's leftovers.

Article


Add your voice to the discussion: