Your app probably has a single activity that appears in the user’s home
screen launcher. It is the activity that has the <intent-filter>
for the MAIN
action and the LAUNCHER
category.
For years, many home screens for Android have allowed the user to make “shortcuts” to that activity, typically by long-pressing the icon in the launcher, then dragging it to the desired spot on the home screen. This is reminiscent of similar capabilities in many desktop operating systems.
However, some desktops have gone beyond that. For example, with the Unity desktop in Linux, right-clicking a launcher icon in the Unity dock may bring up specific ways to get into the app identified by that icon. For example, an email client might offer “Compose New Message” from the icon’s context menu, so whereas a simple click on the icon would bring up the inbox, right-clicking and choosing “Compose New Message” would bring up a message composer.
Android 7.1 adds the awkwardly-named “app shortcuts” to mimic this sort of feature. There are two ways of adding these shortcuts: via a resource tied into the manifest, and via Java code. The former approach has no particular ties to Android 7.1, and third-party home screen implementations are already adopting it.
In this chapter, we will explore what app shortcuts are, how to add them to the manifest, and how to offer “dynamic” app shortcuts from Java.
Understanding this chapter requires that you have read the chapter on
Intent
filters.
Google has been steadily increasing the ways in which users can drive directly into specific portions of your app, as opposed to always getting into it via a home screen launcher or perhaps the overview screen, such as:
ACTION_SEND
All of these are designed to make it a bit easier for power users to get where they want to go quickly, saving some taps, swipes, or other forms of input.
The app shortcuts added by Android 7.1 work much the same way.
It will help to understand what you are supposed to be adding to your app if you see what the user experience is for apps with app shortcuts.
However, technically, a home screen can do whatever it wants with app shortcuts, from a presentation standpoint. So, let’s focus on the Pixel Launcher first, which is the launcher that Google shipped with their 2016 Pixel phones and offers app shortcut support. The Android 7.1 emulator has a similar launcher.
The user can long-press on an app icon and pull up a list of available app shortcuts:
Figure 889: App Shortcuts for Settings App
If the icon does not support app shortcuts, the long-press simply does whatever it ordinarily would have done prior to app shortcuts. For example, long-pressing an icon in the launcher would allow the user to drag it to be a shortcut on the main home screen. To do those sorts of things with an icon that does support app shortcuts, you not only need to long-press but also start dragging the icon somewhere.
Each of those app shortcuts has a small “grab handle” (looks like =
).
The user can drag that and use it to create a shortcut on the home screen
for that particular app shortcut:
Figure 890: Pinned Battery App Shortcut from Settings App
Tapping that icon directly launches whatever the app shortcut has specified.
However, developers are limited only by their imaginations in terms
of presentation of app shortcuts. Home screens have easy access to app
shortcut information via the LauncherApps
utility class, and there
is little stopping other apps from doing the same. So, you can imagine:
And so on.
The “low-hanging fruit” of app shortcuts is to offer some static options via the manifest. This takes very little time to implement, including no mandatory Java code changes. Furthermore, while the Android 7.1 APIs for working with app shortcuts may not exist on older devices, home screens and other apps could still support manifest app shortcuts with a bit of additional code. Hence, the app shortcuts that you offer via the manifest will become available to the users of many popular alternative home screen implementations, in addition to users of Android 7.1+ devices.
The
AppShortcuts/WeakBrowser
sample project demonstrates the use of both manifest and dynamic app
shortcuts. This app implements a silly little Web browser, allowing
the user to visit a handful of hard-coded sites.
First, you need to decide where these app shortcuts should send users.
From a navigation flow standpoint, an app shortcut:
For example, suppose that a common bit of existing navigation in your app is:
Adding an app shortcut to that same destination is easy but not that useful, as it will be no faster — and perhaps slower — to activate the app shortcut than it would to be to just go into the activity and tap on the desired tab.
But, the navigation might be more complex, where getting to the destination:
Now offering rapid access to the destination via an app shortcut may be useful, as it may be faster than the ordinary navigation options.
Of course, WeakBrowser
, being weak, has one manifest app shortcut:
to allow the user to visit a search engine. This same page is available
by tapping a “search” action bar item. This is not an especially
effective use of manifest app shortcuts, but it helps to simplify
the example.
App shortcuts offered via the manifest are static. You cannot modify them at runtime, the way that you can with dynamic app shortcuts. Hence, they cannot really be personalized.
Also, if the user pins one of these app shortcuts, and some future version of your app eliminates the app shortcut, the pinned app shortcut may remain on the user’s home screen. Tapping it would display some sort of “you cannot do this anymore” message. From a user experience standpoint, this will not be popular.
So, try to have your manifest app shortcuts be “evergreen”, ones that are unlikely to need to be changed or removed in the future.
An app shortcut triggers a call to startActivity()
on some Intent
.
With manifest app shortcuts, you describe the Intent
in XML, and some
other process creates that Intent
and passes it to startActivity()
.
This means that any destination that you want to offer needs to be able
to be reached by some startActivity()
call, with an Intent
that can be
built out of
some combination of the following:
Uri
(the data
facet of an Intent
)Notably, it appears that you cannot use extras or categories to distinguish
this Intent
from any other that starts up the same activity.
Also, this activity will need to be exported, as third-party apps will
need to be able to start up the activity. If the activity has an
<intent-filter>
, it will be exported. Otherwise, you will need to add
android:exported="true"
to the <activity>
in the manifest.
The manifest app shortcuts are defined via an XML resource, usually
residing in res/xml/
within your module’s main/
sourceset. This
will contain a root <shortcuts>
element, which itself contains
one or more <shortcut>
elements:
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:icon="@drawable/ic_search_black_24dp"
android:shortcutId="search"
android:shortcutLongLabel="@string/search_long_desc"
android:shortcutShortLabel="@string/search">
<intent
android:action="android.intent.action.SEARCH"
android:targetClass="com.commonsware.android.appshortcuts.MainActivity"
android:targetPackage="com.commonsware.android.appshortcuts" />
</shortcut>
</shortcuts>
Here, we have a single app shortcut. The required attributes are:
android:shortcutId
, for a unique identifier for this app shortcutandroid:icon
, for a launcher-style icon for this app shortcutandrodid:shortcutShortLabel
, which will be the caption for the
app shortcut iconandroid:shortcutLongLabel
is optional. In theory, it will be used
in places where a longer description of the app shortcut may be useful.
In practice, it is unclear where this would be used.
The other required piece of a shortcut definition is the nested
<intent>
element. This describes what Intent
should be used with
startActivity()
to take the user to where this app shortcut advertises
as its destination. Typically, you will use the three attributes
shown in the above sample:
android:action
, for an action string, as this is required, even if
it is totally uselessandroid:targetClass
and android:targetPackage
, to provide the
pieces of the ComponentName
to identify the activity to be startedA shortcut can have several <intent>
elements, which will cause Android
to create a fake back stack for the user (i.e., pressing BACK from the
last <intent>
will take the user to whatever activity was identified
in the preceding <intent>
). And, a <shortcuts>
element can have
one or several <shortcut>
elements. However, bear in mind that a
launcher may not use many app shortcuts — for example, the Pixel Launcher seems to
cap the presentation at three app shortcuts. These results will vary
by launcher (or other app shortcuts client) but you should assume that
you only have so many app shortcut “slots” to display to the user.
Then, you need to add a <meta-data>
element to your <activity>
element for your launcher activity in the manifest, pointing Android
to your XML resource:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
Only app shortcuts declared on launcher activities will be honored.
If you try putting this <meta-data>
element on other activities, it
will be ignored. If your app is one of the few with multiple launcher icons,
each could have its own app shortcuts. Or, you might take this opportunity
to consolidate those launcher icons into a single one, with the secondary
launcher icons turning into app shortcuts.
The fact that this is just a <meta-data>
element and an XML resource
is why existing home screens could adopt manifest app shortcuts.
All of this information is available via PackageManager
, going back
to the earliest Android versions.
If you install this app on a device with a compatible home screen implementation, the manifest app shortcut should be available, such as what you get on the Pixel:
Figure 891: Manifest App Shortcut for WeakBrowser
The biggest problem comes with the icons. There are no instructions at all as what these icons should look like, or what size they should be. The author’s assumption is that they should be launcher-style icons is made in part by the behavior of when you use other types of icons, such as the simple action bar-style search icon:
Figure 892: Pinned Manifest App Shortcut for WeakBrowser
Here, the app shortcut was pinned to the home screen, and the icon looks… unpleasant.
Also note that while the Pixel Launcher superimposes your app’s icon over the app shortcut icon, it is unclear if that is something that is required by the framework or merely a Pixel Launcher convention.
So, you ship an update to your app, where you declare some manifest app shortcuts. Some time later, you revamp the UI of your app, and one of those app shortcuts no longer makes sense. It might not even work anymore — for example, you might have removed support for the activity that the app shortcut pointed to.
You might think that whatever shortcut XML you use in the new app version is what the device will use, once the user upgrades to the new app version. That is true, with one noteworthy exception: pinned app shortcuts. Google does not want these to vanish into thin air based on an app update, as that might confuse the user.
You have two main options for how to handle this gracefully:
<shortcut>
with the same android:shortcutId
attribute,
but give it whatever icon, labels, and <intent>
are appropriate.
Users who upgrade your app will have their pinned shortcut updated
to reflect the new settings. This works well in cases where the
app shortcut has changed a bit but still closely resembles its original
role.<shortcut>
for the
old ID. Preferably, though, you have a <shortcut>
for the to-be-disabled
ID. On that element, you can have android:enabled="false"
to indicate
that the app shortcut is now disabled, and you can have
android:shortcutDisabledMessage
pointing to a string resource where
you explain why that app shortcut has been disabled. If the user taps
on the app shortcut, this message should appear.Truly personalized app shortcuts usually cannot be specified in the manifest. For example, you may want to allow the user to have an app shortcut to their favorite “friend” in your social network client. The identity of that friend varies by user and time. While you could offer a manifest-registered app shortcut for “Favorite Friend”, the user will not know necessarily who that friend is. With dynamic shortcuts, you can craft one that uses the name and avatar of that specific friend.
Offering dynamic app shortcuts is more powerful and correspondingly more complex.
Our WeakBrowser
sample app, on initial install, only has the one
manifest app shortcut. However, the user can visit a Settings activity
within the app and elect to enable “bookmarks”:
Figure 893: WeakBrowser Settings Activity
The user can choose which bookmarks to use from a multi-selection preference:
Figure 894: WeakBrowser Settings Activity, Showing Bookmarks
If the user checks some bookmarks, they get added as dynamic app shortcuts, to go along with the existing manifest app shortcut:
Figure 895: WeakBrowser Manifest and Dynamic App Shortcuts
An app shortcut is “pinned” if, by one means or another, the user has indicated that they want long-term direct access to whatever that app shortcut represents. In the Pixel Launcher, an app shortcut is pinned if the user grabs the grab handle and drags it as a shortcut onto the home screens. Different home screens will have different visual metaphors for “pinned”.
An app shortcut is “immutable” if it cannot be changed by the app that provided (“published”) the app shortcut. Manifest app shortcuts are immutable. Conversely, an app shortcut is “mutable” if its contents can be changed. Dynamic app shortcuts are mutable.
Each app shortcut has a unique ID. For manifest app shortcuts, that is set
via the android:shortcutId
attribute in the <shortcut>
element.
Dynamic app shortcuts have an equivalent means of establishing their ID.
As you manipulate dynamic app shortcuts, what happens depends upon:
As with manifest app shortcuts, you need to know where you are going
to send the user within your app when the user chooses one of your dynamic app
shortcuts. However, in this case, you will be able to provide a full
Intent
associated with the app shortcut. In principle, you could use
things like extras, whereas that is not documented to be supported for
manifest app shortcuts.
As with manifest app shortcuts, the destination for your dynamic app shortcuts
needs to be identifiable by an Intent
that will be used with startActivity()
to take the user to that destination. However, unlike with manifest app
shortcuts, you have full control over the setup of the Intent
that is
used for dynamic app shortcuts.
In particular, you may want to consider what Intent
flags to use.
A manifest app shortcut will have FLAG_ACTIVITY_NEW_TASK
and
FLAG_ACTIVITY_CLEAR_TASK
added to the Intent
constructed from the
shortcut XML. This will send the user to your destination, wiping out
the back stack from that task. You might elect to use other flags, or
control things using <activity>
manifest attributes like android:taskAffinity
,
to get the flow that you want.
For the bookmarks, our sample app has a model class, named Bookmark
,
much to nobody’s surprise:
package com.commonsware.android.appshortcuts;
import java.util.HashMap;
class Bookmark implements Comparable<Bookmark> {
static final HashMap<String, Bookmark> MODEL=new HashMap<>();
final String url;
final String title;
final String id;
static {
add(new Bookmark("Android Developer Home",
"https://developer.android.com",
"687a9ea6-f0c0-448c-9cc9-a4aa6e10a1af"));
add(new Bookmark("Android Open Source Project",
"https://source.android.com",
"0ee37e25-2dac-4602-8aa2-3709ac4037c8"));
add(new Bookmark("AOSP Source Search",
"http://xref.opersys.com/",
"405ba533-337e-40be-abe0-fb86cd04bf7d"));
add(new Bookmark("Stack Overflow Android Questions",
"https://stackoverflow.com/questions/tagged/android",
"c9599794-cb9f-46a1-ad61-971ff2a8a172"));
add(new Bookmark("The CommonsBlog",
"https://commonsware.com/blog/",
"948fe25a-44d4-49d0-a23f-2783f786040d"));
add(new Bookmark("CWAC Community",
"https://community.commonsware.com/c/cwac",
"4c7fac0f-fc86-4c68-8ad8-99198fc3d433"));
}
private static void add(Bookmark b) {
MODEL.put(b.id, b);
}
Bookmark(String title, String url, String id) {
this.url=url;
this.title=title;
this.id=id;
}
@Override
public int compareTo(Bookmark bookmark) {
return(title.compareTo(bookmark.title));
}
}
Here, to keep the example simple, the “database” of bookmarks is merely
hardcoded roster, stored in a HashMap
, keyed by a UUID serving as a
unique identifier. In addition to its id
, each Bookmark
has a
title
and a url
.
When it comes time to build an Intent
for a given Bookmark
, we use
that url
as the “data” facet of the Intent
, to deliver it to our
MainActivity
:
private Intent buildIntent(Bookmark item) {
return(new Intent(getActivity(), MainActivity.class)
.setAction("i.can.haz.reason.why.this.is.REQUIRED")
.setData(Uri.parse(item.url)))
.putExtra(MainActivity.EXTRA_BOOKMARK_ID, item.id)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
We also:
MainActivity
Intent
that are used by manifest app
shortcuts, to synchronize the behavior between our one manifest app
shortcut and any dynamic app shortcuts that we createAndroid 7.1’s SDK offers a ShortcutInfo.Builder
, which lets you create
ShortcutInfo
objects, each of which represents one dynamic app shortcut.
Given a Set
of Bookmark
IDs, we can craft the corresponding
ShortcutInfo
objects via builders:
private List<ShortcutInfo> buildShortcuts(Set<String> ids) {
List<Bookmark> items=new ArrayList<>();
for (String id: ids) {
items.add(Bookmark.MODEL.get(id));
}
if (items.size()>0) Collections.sort(items);
List<ShortcutInfo> shortcuts=new ArrayList<>();
for (Bookmark item : items) {
shortcuts.add(new ShortcutInfo.Builder(getActivity(), item.id)
.setShortLabel(item.title)
.setIcon(buildIcon(item))
.setIntent(buildIntent(item))
.build());
}
return(shortcuts);
}
The ShortcutInfo.Builder
constructor takes a Context
for resource
resolution, plus a unique ID of the app shortcut. In our case, we just
use the unique ID of the Bookmark
, since a Bookmark
corresponds 1:1
with our dynamic app shortcuts.
The builder methods that we use here mirror the XML that we used in the manifest app widget:
Manifest App Widget XML | Builder Method |
---|---|
android:shortcutShortLabel |
setShortLabel() |
android:icon |
setIcon() |
<intent> |
setIntent() |
Our setIntent()
call uses the buildIntent()
method shown in the
preceding section. In theory, our corresponding buildIcon()
method
would craft an icon for each bookmark, perhaps using the favicon
of the site. Here, we just use a simple resource image, the same one
for each bookmark:
private Icon buildIcon(Bookmark item) {
return(Icon.createWithResource(getActivity(),
R.drawable.ic_bookmark_border_black_24dp));
}
This buildShortcuts()
method simply creates the ShortcutInfo
objects.
To apply them, we need to get our hands on a ShortcutManager
, via
getSystemService()
:
shortcuts=getActivity().getSystemService(ShortcutManager.class);
Then, we can call setDynamicShortcuts()
on the ShortcutManager
,
supplying our list of ShortcutInfo
objects, to specify that this list
of ShortcutInfo
objects represents the current roster of dynamic app
shortcuts to offer to the user:
private void showBookmarks() {
updateBookmarks(bookmarks.getValues());
}
private void updateBookmarks(Set<String> ids) {
shortcuts.setDynamicShortcuts(buildShortcuts(ids));
}
All of this code is appearing in a SettingsFragment
that shows the
SwitchPreference
and MultiSelectListPreference
for manipulating
the bookmarks. showBookmarks()
is called if the user toggles on the
SwitchPreference
, and updateBookmarks()
is called when the user
changes which items are checked in the MultiSelectListPreference
(held in the bookmarks
field).
It is possible that you will want to remove some existing dynamic app shortcuts. In the case of the sample app, there are two possibilities:
SwitchPreference
, meaning that no dynamic app
shortcuts should be offeredScenario #1 is handled just by calling setDynamicShortcuts()
on the ShortcutManager
, as this
removes any existing app shortcuts.
Scenario #2 is handled by removeAllDynamicShortcuts()
on the ShortcutManager
, which does pretty
much what the method name suggests and removes all dynamic app
shortcuts:
private void hideBookmarks() {
shortcuts.removeAllDynamicShortcuts();
}
Another option is the removeShortcuts()
method on the ShortcutManager
.
This takes a List
of ID values and removes those app shortcuts.
However, bear in mind that:
What these methods are doing is changing the roster of dynamic app shortcuts that are available to the user from the launcher icon. They do not affect any existing pinned app shortcuts.
ShortcutManager
has a handful of other methods that allow you to
manipulate the roster of dynamic app shortcuts that your app produces.
addDynamicShortcuts()
will update any dynamic app shortcuts with the
same IDs as the ones that you supply, and any new dynamic app shortcuts
will be added. Where setDynamicShortcuts()
says “replace the existing roster
with this one”, addDynamicShortcuts()
says “update or augment the
existing roster with these, and leave everything else alone”.
updateShortcuts()
is like addDynamicShortcuts()
, except that it will
only update existing dynamic app shortcuts, not add new ones.
reportShortcutUsed()
should be called, with the ID of a shortcut, whenever
that app shortcut gets used by the app. In theory, this information might
help Android optimize the presentation of app shortcuts to the user, though
it is unclear if this is being used at the moment. This is why we put
the bookmark ID in the EXTRA_BOOKMARK_ID
extra: so MainActivity
can report the usage of this app shortcut.
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.N_MR1) {
String id=i.getStringExtra(EXTRA_BOOKMARK_ID);
if (id!=null) {
getSystemService(ShortcutManager.class)
.reportShortcutUsed(id);
}
}
(here, i
is the Intent
used to display this activity)
disableShortcuts()
— where you supply it with a list of dynamic app
shortcut IDs — allows you to stop pinned dynamic app shortcuts from working.
While setDynamicShortcuts()
, removeDynamicShortcuts()
, and
removeAllDynamicShortcuts()
affect the roster of available dynamic app
shortcuts, they do not affect any pinned dynamic app shortcuts. Those will
still work. If the reason why you are removing some dynamic app shortcuts
is that the user is no longer eligible for those things (e.g., the user
failed to renew a subscription), disableShortcuts()
allows you to block
those dynamic app shortcuts from working. The user will be shown a message
instead of having the pinned dynamic app shortcut launch an activity, and
you can tailor that message if desired.
setDynamicShortcuts()
, updateShortcuts()
, and addDynamicShortcuts()
all do the same thing if there is an existing dynamic app shortcut with
the same ID: update its contents to reflect whatever you passed in to those
methods. This includes “updating” it to have the same information as it
already has, if you have not changed anything. These not only update
the roster that will be shown to the user, but they also update
any pinned editions of those dynamic app shortcuts.
This introduces a potential area of confusion for the user.
For example, a Web browser that is more sophisticated than is WeakBrowser
could keep track of which sites the user visits most often. Then,
the browser could offer a dynamic app shortcut to visit that specific
site. Let’s pretend for a moment that, at some point in time, the user’s
most-visited site is the CommonsWare site.
The browser would have a dynamic app shortcut for “CommonsWare”, which
the user could pin.
Some time later, as the user continues using the browser, the browser realizes that some new site has supplanted the CommonsWare site as the one that the user has visited the most. So, the browser will want its dynamic app shortcut roster to reflect this change.
There are two ways of going about this:
mostPopular
)
and change its label and Intent
to reflect the new most-popular site.
However, this will not only change what the user sees when looking at
the available app shortcuts, but it also changes the pinned app shortcut.
Now that home screen shortcut would bring up some other site, while the
user had pinned the CommonsWare site.setDynamicShortcuts()
now, the list of shortcuts would contain one
with the new app shortcut ID and would not contain one with the old app
shortcut ID. The list of dynamic app shortcuts that the user sees will
reflect this change. But, since the browser did not change any data
with the old app shortcut ID, the pinned one remains as it was, still
pointing to the CommonsWare site.If this sort of thing sounds like it might be plausible for your planned use of dynamic app shortcuts, you probably want to consider the app shortcut ID to be tied to the content (e.g., the site URL), not the role the content is being applied to (e.g., the most-popular site URL).
There are three getter methods that allow you to find out what app shortcuts are outstanding and are related to your app:
getDynamicShortcuts()
getManifestShortcuts()
getPinnedShortcuts()
The latter, as the name suggests, lets you know which app shortcuts (manifest or dynamic) have been pinned by the user.
Note that the ShortcutInfo
objects that you get back from these methods
may not have all of their details filled in (e.g., may be missing the icon).
Mostly, you will be looking for the shortcut IDs, so you can make determinations
of how to manipulate the app shortcut roster (e.g., do you need to disable
anything?).
Unfortunately, there are some aspects of dynamic app shortcuts which will require additional work.
If your labels for the dynamic app shortcuts have translations for other languages,
you need to replace the dynamic app shortcuts when the user switches the
device to a different locale. For example, you could have a manifest-registered
BroadcastReceiver
, listening for the ACTION_LOCALE_CHANGED
system
broadcast. There, you could call setDynamicShortcuts()
or updateShortcuts()
to reflect the new labels.
However, outside of locale changes, you need to be careful about
updating dynamic app shortcut information when your app is in the
background. There is a “rate limit” established that will prevent
you from changing those shortcuts too frequently. addDynamicShortcuts()
,
setDynamicShortcuts()
, and updateShortcuts()
each return a boolean
;
false
indicates that your request failed due to rate-limiting. You can also
call isRateLimitingActive()
on the ShortcutManager
to find out in
advance whether your app is being rate-limited and would not be able
to affect dynamic app shortcut changes.
Also, there is a limit of how many app shortcuts you can have at any
one time. geMaxShortcutCountPerActivity()
on ShortcutManager
reports this limit. Attempting to go past that will result in an exception.
Android 7.1 appears to have a limit of five; if you try enabling all six
bookmarks in the sample app, you will crash.
Bear in mind that information contained in app shortcuts will be visible
to home screens and anything else using LauncherApps
to get at the possible
app shortcuts. As such, please be careful to avoid putting sensitive information
in app shortcuts (e.g., labels).