Categories UI & UX

How to easily implement Android Q Bubbles

4 Apr. 2019
2
0
19 minutes
Android Q Bubbles

A few weeks ago, Google introduced the first Beta of Android Q. Thousands of bugs and suggestions have been reported since the first release and Android team made some changes based on these feedbacks. Android opened Android Q Developer Preview 2 on April 3, 2019. This second version includes interesting features that we tested.

Android Q

This post comes after two posts about Android Q Settings Panels and Android Q Location Permissions:

Android Q Beta 2 includes bug fixes, API updates and general optimizations. It also brings some new features such as a foldable emulator, directional and zoomable microphones as well as Bubbles. The post aims at exploring Android Q Bubbles.

You can find the release notes of Android Q Beta 2 on Android Developer website. Here is a video that highlights many of the changes in Android Q Beta 1 and 2:

For now, Android Q Beta 2 is only available on Google Pixel devices (Pixel 3, Pixel 3 XL, Pixel 2, Pixel 2 XL, Pixel, or Pixel XL). Nevertheless, it is possible to set up an Android Emulator to run Android Q Beta. The procedure is described in Get Android Q Beta article.

Introduction

Android Q adds platform support for Bubbles. It is a new way for users to easily multitask anywhere on their device. Various apps already use similar interactions and it has the advantage of re-engaging users with apps. The Android team built Bubbles to make interactions consistent and reduce development time. Android Q Bubbles help users prioritize information and perform actions within another app without changing their current context. It allows users to move between apps and activities while keeping control over some functionalities they value.

Android Q Bubbles float on top of other app content and follow the user wherever they go. These Bubbles can be expanded to reveal app features, and collapsed to focus on another action.

For instance, a great use case would be messaging bubbles. It let users keep important conversations within easy to reach while navigating in other apps. Android Q Bubbles could also provide quick access to portable User Interfaces such as notes, reminders or translations.

Android Q Bubbles
Android Q Bubbles

In this post, we will implement a messaging bubble from scratch. Here is a preview of our project:

Android Q messaging Bubble

Video

Setup

First and foremost, you need to update the new Android Q SDK. In order to achieve this, two updates are required in Tools > SDK Manager. Android Q Preview API Level 2 (or higher) must be installed in SDK Platforms tab. Furthermore, Android SDK Build-Tools 28 (or higher) is also required. It can be installed in SDK Tools tab.

Afterwards, it is necessary to update the build configuration of your app as follows:

android {
    compileSdkVersion 'android-Q'

    defaultConfig {
        targetSdkVersion 'Q'
        minSdkVersion 'Q'
    }
    ...
}

Implementation

This post focuses on the implementation of a messaging bubble. Basically, there will be a single button that generates a messaging bubble that allows you to talk with a parrot bot. As you may be wondering, this parrot will repeat anything you say. Here is a demonstration of our project:

Android Q messaging Bubble demonstration

The code of this project is fully available on the Github repository linked at the end of this post. Given that this parrot bot requires other features such as Room database, LiveData and ViewModel that aren’t related to this topic, this post will only focus on Bubble implementation. All the remaining code is on Github.

Android Q Bubbles are built on top of Android’s notification system to provide a familiar and easy to use API for developers. Therefore, Bubbles can be created in the same way as Notifications. You just need to attach extra data to displays Bubbles.

Note that when the device is locked, Bubbles appear just as a notification normally would. This applies also to the case where the always-on-display mode is active.

When an app displays a bubble for the first time, a permission dialog is shown. This dialog offers two choices:

  • Block all bubbles from the app: Notifications are not blocked, but they will never appear as bubbles.
  • Allow all bubbles from the app: All notifications sent with BubbleMetaData will appear as bubbles.

Here is an insight of this permission dialog:

Bubble permission dialog

Activity configuration

A Bubble is created from an activity of your choice. An activity can be displayed inside a Bubble only if it is configured properly. The activity must be resizeable and embedded, and always launch in document UI mode. If one of these requirements isn’t met, it will display a notification instead.

The following code demonstrates how to configure an activity to be shown in a Bubble:

<manifest ...>
    <application ...>
        ...
        <activity
            android:name=".ui.ChatActivity"
            android:allowEmbedded="true"
            android:documentLaunchMode="always"
            android:resizeableActivity="true" />
    </application>
</manifest>

Some apps may show multiple bubbles of the same time, which is a common use case in messaging apps where there are multiples bubbles for with different conversations. In that case, the activity must be able to launch multiple instances. Therefore documentLaunchMode must be set to “always”. Once an activity meets these requirements, it can display whatever you want.

Notification Channel

As Bubbles are built on top of the Notifications API, they must be assigned to a channel or they will not appear. Notification Channels are used to categorize notifications and let users disable specific types of notifications instead of disabling them all.

In our project, we created a special classes that performs various operations related to Notifications. This includes the Notification Channel creation as well as the Bubble creation. Here is the code that is used to create a Notification Channel:

class NotificationHelper(private var context: Context) {
    private var notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java)

    fun setUpNotificationChannel(
        channelId: String, channelName: String, description: String,
        @ChannelImportance importance: Int
    ) {
        if (notificationManager.getNotificationChannel(channelId) == null) {
            val notificationChannel = NotificationChannel(channelId, channelName, importance)
            notificationChannel.description = description
            notificationManager.createNotificationChannel(notificationChannel)
        }
    }

    @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
    @IntDef(
        NotificationManager.IMPORTANCE_DEFAULT,
        NotificationManager.IMPORTANCE_NONE,
        NotificationManager.IMPORTANCE_MIN,
        NotificationManager.IMPORTANCE_LOW,
        NotificationManager.IMPORTANCE_HIGH,
        NotificationManager.IMPORTANCE_MAX,
        NotificationManager.IMPORTANCE_UNSPECIFIED
    )
    private annotation class ChannelImportance
}
public class NotificationHelper {

    private Context context;
    private NotificationManager notificationManager;

    public NotificationHelper(Context context) {
        this.context = context;
        notificationManager = context.getSystemService(NotificationManager.class);
    }


    public void setUpNotificationChannel(String channelId, String channelName, String description,
                                         @ChannelImportance int importance) {
        if (notificationManager.getNotificationChannel(channelId) == null) {
            NotificationChannel notificationChannel = new NotificationChannel(channelId, channelName, importance);
            notificationChannel.setDescription(description);
            notificationManager.createNotificationChannel(notificationChannel);
        }
    }

    @Retention(SOURCE)
    @IntDef({NotificationManager.IMPORTANCE_DEFAULT,
            NotificationManager.IMPORTANCE_NONE,
            NotificationManager.IMPORTANCE_MIN,
            NotificationManager.IMPORTANCE_LOW,
            NotificationManager.IMPORTANCE_HIGH,
            NotificationManager.IMPORTANCE_MAX,
            NotificationManager.IMPORTANCE_UNSPECIFIED})
    private @interface ChannelImportance {
    }
}

As you can see, this utility class has a setUpNotificationChannel() method that creates a Notification Channel if it doesn’t already exist. It takes a channel id, a name, a description and an importance level. This last parameter is constrained to the available values thanks to IntDef. Note that the context seems to be useless there, but another method that we will study in a very short time uses it.

Bubble creation

Note that the first time you send the notification to display a bubble, it has to be in a notification channel with IMPORTANCE_HIGH.

To send a bubble through a notification you need to add a BubbleMetadata by calling setBubbleMetadata with the Notification.Builder.

fun showBubble(channelId: String, target: Intent, title: String, iconRes: Int) {
    val bubbleIntent = PendingIntent.getActivity(
        context,
        REQUEST_BUBBLE,
        target,
        PendingIntent.FLAG_UPDATE_CURRENT
    )

    val bubbleMetadata = Notification.BubbleMetadata.Builder()
        .setIcon(Icon.createWithResource(context, iconRes))
        .setIntent(bubbleIntent)
        .setDesiredHeight(context.resources.getDimensionPixelSize(R.dimen.bubble_height))
        .build()

    val builder = Notification.Builder(context, channelId)
        .setContentTitle(title)
        .setSmallIcon(R.drawable.ic_message)
        .setCategory(Notification.CATEGORY_MESSAGE)
        .setShowWhen(true)
        .setBubbleMetadata(bubbleMetadata)
        .setContentIntent(
            PendingIntent.getActivity(
                context,
                0,
                target,
                PendingIntent.FLAG_UPDATE_CURRENT
            )
        )

    notificationManager.notify(0, builder.build())
}

companion object {
    private const val REQUEST_BUBBLE = 2
}
private final static int REQUEST_BUBBLE = 2;

public void showBubble(String channelId, Intent target, String title, int iconRes) {
    PendingIntent bubbleIntent = PendingIntent.getActivity(context,
            REQUEST_BUBBLE,
            target,
            PendingIntent.FLAG_UPDATE_CURRENT);

    Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder()
            .setIcon(Icon.createWithResource(context, iconRes))
            .setIntent(bubbleIntent)
            .setDesiredHeight(context.getResources().getDimensionPixelSize(R.dimen.bubble_height))
            .build();

    Notification.Builder builder = new Notification.Builder(context, channelId)
            .setContentTitle(title)
            .setSmallIcon(R.drawable.ic_message)
            .setCategory(Notification.CATEGORY_MESSAGE)
            .setShowWhen(true)
            .setBubbleMetadata(bubbleMetadata)
            .setContentIntent(
                    PendingIntent.getActivity(
                            context,
                            0,
                            target,
                            PendingIntent.FLAG_UPDATE_CURRENT
                    )
            );

    notificationManager.notify(0, builder.build());
}

To create a Bubble, we need a channel id, an intent, a title as well as an icon. First, a PendingIntent must be created with the specified intent. It includes the flag PendingIntent.FLAG_UPDATE_CURRENT in order to replace the PendingIntent data with the new Intent if it already exists. Then, a BubbleMetatdata object must be created. Within the metadata, you can provide various data as listed below:

Once the BubbleMetadata is created, we can assign it to the Notification.Builder using setBubbleMetadata() method. You can set the activity that will be shown when the user clicks on the expand button using setContentIntent(contentIntent).

Test

Once these methods are set, we can use it to create and display a custom bubble as follow:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val notificationHelper = NotificationHelper(this)

    notificationHelper.setUpNotificationChannel(
        CHANNEL_MESSAGES, getString(R.string.channel_new_messages),
        getString(R.string.channel_new_messages_description), NotificationManager.IMPORTANCE_HIGH
    )

    chat_bubble_button.setOnClickListener {
        notificationHelper.showBubble(
            CHANNEL_MESSAGES,
            Intent(this, ChatActivity::class.java),
            getString(R.string.parrot),
            R.drawable.parrot
        )
    }
}

companion object {
    private const val CHANNEL_MESSAGES = "new_messages"
}
private final static String CHANNEL_MESSAGES = "new_messages";

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Button chatBubbleButton = findViewById(R.id.chat_bubble_button);

    final NotificationHelper notificationHelper = new NotificationHelper(this);

    notificationHelper.setUpNotificationChannel(CHANNEL_MESSAGES, getString(R.string.channel_new_messages),
            getString(R.string.channel_new_messages_description), NotificationManager.IMPORTANCE_HIGH);

    chatBubbleButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            notificationHelper.showBubble(CHANNEL_MESSAGES,
                    new Intent(MainActivity.this, ChatActivity.class),
                    getString(R.string.parrot),
                    R.drawable.parrot);
        }
    });
}

Note that the Notification Channel must be created before trying to show Bubbles.

Download

  • Kotlin
  • Java

Conclusion

In conclusion, Android Q Bubbles are new components that enhance the way users multitask. It allows users to choose information or actions they value in order to keep them within easy reach wherever they go. There are many interesting use cases that could transform how users interact with apps. I think this is a great feature that increases the opportunities for app developers. And you? How would you use Android Q Bubbles? Feel free to share your idea!

Leave a Reply

Your email address will not be published. Required fields are marked *