Quick Settings Tiles on Android 7.0

Requiring that a user open your app is so 2008. Widgets and notifications have been around since the early days of Android to provide additional surfaces for displaying important controls and information from your app even when the app isn’t open. New in Android 7.0 (API 24), any app can now create a quick settings tile for quick access to critical pieces of functionality available above the notification tray.

Besides being an always-up-to-date surface for information from your app, tapping on a tile makes it possible to trigger background work, open a dialog, or even open an activity.

What makes a good quick settings tile

There’s two main things to keep in mind when deciding on whether to build a quick settings tile: the urgency of the action and the frequency of the action. While it should be obvious that the best tiles are ones that are both high urgency and high frequency, a high urgency but not very frequent action could still be considered as a valid quick settings tile. Since frequency is often very different from user to user (one user might use Google Cast multiple times a day, another once a week), prioritize the urgency and importance of ease of access (both to the information and the action) when deciding what makes a good quick settings tile.

Since a quick settings tile serves as a persistent way to interact with your app, make sure that your tile is useful as long as the app is installed. That means they wouldn’t be appropriate for one-time activities such as setup tasks.

Building a TileService

Each tile is associated with a TileService which is how the system communicates the status of your tile and how you push updates to your tile. Like any Service, you must declare it in your manifest:

<service
android:name=".AwesomeTileService"
android:icon="@drawable/ic_tile_default"
android:label="@string/tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action
android:name="android.service.quicksettings.action.QS_TILE"/>
</intent-filter>
</service>

One thing to note is that the android:icon and android:label are actually very critical parts of your TileService. It is those values that are shown when the user is picking which tiles to add to the quick settings pane. Both the icon and label should tell users what the tile actually does — duplicating your app icon and app name is a quick way to never get any users.

Guess which one of these is the good example. That’s right, the one with a representative icon and label

Ideally, the label you choose is short (less than 18 characters or it’ll get truncated) and the icon is a vector drawable that is solid white on a transparent background (think of it as if only the alpha channel mattered). Remember that your tile will only be used on API 24+ devices so this is the perfect time to avoid adding yet another PNG to your app.

The life of a TileService

A TileService is a bound service and hence, its lifecycle is controlled primarily by the Android system. There are three phases to a TileService: being added, listening, and being removed.

The first interaction between the system and your tile is when the user first adds your tile to their quick settings pane. At this point, the system will bind to your TileSevice and call onTileAdded(). This is the appropriate place to do any one-time initialization.

After you tile is added, any time your tile becomes visible you’ll get a callback to onStartListening(). At this point, you’ll be expected to keep your tile to updated, registering any listeners you need. You’ll want to keep those going until you get a callback to onStopListening() — an indication that you’re no longer visible.

The final phase of a tile which I hope you never get to see is when your tile is removed and you get a callback to onTileRemoved(). At this point, your app should stop doing anything tile related until the user re-adds your tile (maybe it was just a mistake? We can only hope).

It is important to note that your TileService will mostly certainly be unbound (and destroyed) when the user is not viewing the tile — don’t assume that your Service will be alive outside of the onStartListening()/onStopListening() pair of methods.

The ~other~ life of a TileService: active mode

Binding to your TileService whenever your tile is visible is the default mode, but if you know exactly when your tile needs to be updated (say, it is based on a background data sync), strongly considering use active mode, which you can enable by adding the META_DATA_ACTIVE_TILE to your manifest entry:

<service
android:name=".AwesomeActiveTileService"
android:icon="@drawable/ic_tile_default"
android:label="@string/tile_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action
android:name="android.service.quicksettings.action.QS_TILE"/>
</intent-filter>
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
</service>

In active mode, your TileService will still be bound for onTileAdded() and onTileRemoved() (and for click events). However, the only time you’ll get a callback to onStartListening() is after you call the static TileService.requestListeningState() method. You’ll then be able to update your tile exactly once before receiving a callback to onStopListening(). This gives you an easy one-shot ability to update your tile right when your data changes whether the tile is visible or not.

Since active tiles don’t have to be bound every time the tile becomes visible, active tiles are better for system health. Building active tiles means less processes the system needs to bind to every time the quick settings panel becomes visible. (Of course, the system already limits the amount of bound TileServices based on available memory, etc, but by then you’re already near the border of memory thrashing — not where you want to be.)

Updating your tile

Once you’re in that zone between onStartListening() and onStopListening(), you’ll be able to update your tile’s UI. Each TileService has a single Tile which represents the UI of your tile and can be retrieved by calling getQsTile().

The main components for your tile’s UI are the icon, label, and content description (for accessibility). You’ll note the icon uses the the Icon class, introduced in Marshmallow. This allows your icon to be created from a Bitmap, content URI, byte array, file path, or resource via the static createWith methods.

The other component of your UI is the state of your tile. A tile can be in one of three states:

Depending on what state your tile is in, your tile’s icon will automatically be tinted (the exact tinting will be manufacturer specific, but expect STATE_ACTIVE to be the most prominent, STATE_INACTIVE less prominent, and STATE_UNAVAILABLE not at all prominent).

Most importantly: you must call updateTile() to update your tile. This is the cue to the system to parse all of the data in your updated Tile and refresh the UI.

@Override
public void onStartListening() {
Tile tile = getQsTile();
tile.setIcon(Icon.createWithResource(this,
R.drawable.ic_title_started));
tile.setLabel(getString(R.string.tile_label));
tile.setContentDescription(
getString(R.string.tile_content_description);
tile.setState(Tile.STATE_ACTIVE);
  tile.updateTile();
}

Keep in mind that your tile can be displayed above the lock screen and when the device is securely locked. If you have sensitive information, consider checking the value of isSecure() to determine if the device is secure.

Handling Clicks

Of course, unless you’re a unavailable tile, users are going to be able to tap on your tile and trigger an action. When you get a callback to onClick(), you have a couple of options. The most obvious one will be doing some work in the background. Just remember that onClick() is going to be on the UI thread so you should move any heavy work to another thread (or another Service — say an IntentService).

However, quick settings tiles also have a few mechanisms for displaying UI. The first of which is via showDialog() which allows your tile to show a dialog. There’s a few cases where a dialog makes a lot of sense:

  • If your action requires additional input (such as selecting from a set of options)
  • If your action impacts another device or equipment — make sure users are aware they’re going to be changing some other device
  • If your action needs specific user consent (say, before downloading something large over metered connections or uploading data the user has not already agreed to)

Basically, a dialog is about adding context to your action. The worst thing you can do is give users an easy way to shoot themselves in the foot.

If you are keeping in mind the ‘critical+frequent’ focus of quick settings tiles, the ability to start an activity with startActivityAndCollapse() might seem a bit odd. And it would be if you’re doing it every time the user clicks the tile, but it can be quite useful in cases such as in response to a ‘More…’ option in a dialog or if you have a unique UI specifically for when the device is locked (remember: you have a launcher icon, don’t just launch that when your tile is clicked).

There are some limitations when it comes to a locked device though. When isLocked() returns true, you won’t be able to display a dialog and activities must have set FLAG_SHOW_WHEN_LOCKED to display above the lock screen. The unlockAndRun() method can be used to prompt the user to unlock their device, allowing you to run code (such as displaying a dialog) only after they’ve unlocked their device.

Note: You’ll notice some of the system tiles have an additional UI that replaces the whole quick settings pane with a custom UI. Unfortunately, this isn’t available to third party tiles.

Long clicking on your quick settings tile will, by default, go to your app’s ‘App Info’ screen. You can override that behavior by adding an <intent-filter> to one of your activities with ACTION_QS_TILE_PREFERENCES.

Convenience at your fingertips

Quick settings tiles give you a whole new surface for interacting with users and allowing your most ardent users quick access to the critical and frequent operations within your app. Be sure to allow some extra time during your development process to really live with your quick settings tile for a number of days: make sure it continues to remain useful and intuitive through the entire lifetime of the app.

#BuildBetterApps

Follow the Android Development Patterns Collection for more!