engineering

Exploring Android Nougat 7.1 App Shortcuts

Google has brought Android Nougat to its second iteration with the 7.1 version (API 25). This one is not a minor release - as a matter of fact it bundles some interesting features under the hood. One of these extra features is App Shortcuts. This post explores what they are, how they work, and how you can implement them.

The end result looks like this:
app-shortcuts-final

If you want to go through a step by step guide, please read on.

What are App Shortcuts and why would you need them?

App Shortcuts are a means of exposing your application’s common actions or tasks on the user’s home screen. Your users can reveal the shortcuts by long-pressing the app's launcher icon. From a technical perspective, App Shortcuts are a simple and quick way of firing your application’s Intents[^n].

They are of two types:

  • Static: These are shortcuts that are defined statically in a resource file; These cannot be changed unless you modify the file and re-deploy the app
  • Dynamic: These are shortcuts that are published at runtime; These can be updated without the need to re-deploy the app

By exposing your common tasks, you'll make it easier for your users to quickly get back into specific parts of your application without the need of additional navigation.

Adding App Shortcuts

Adding Shortcuts to your app is pretty straightforward. Let's start with creating a simple static shortcut[^n].

Static shortcuts

This example assumes you already have a project set up in Android Studio. Navigate to your AndroidManifest.xml and add the following meta-data tag to your main activity:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.catinean.appshortcutsdemo">


  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />


        <category android:name="android.intent.category.LAUNCHER" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>


      <meta-data
        android:name="android.app.shortcuts"
        android:resource="@xml/shortcuts" />
    </activity>
  </application>


</manifest>

In the meta-data tag, the android:resource key corresponds to a resource defined in your res/xml/shortcuts.xml. Here you need to define all of your static shortcuts. Let's add one that will open a certain activity from your app. In the example below I've created a dummy StaticShortcutActivity:

<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
  <shortcut
    android:enabled="true"
    android:icon="@drawable/ic_static_shortcut"
    android:shortcutDisabledMessage="@string/static_shortcut_disabled_message"
    android:shortcutId="static"
    android:shortcutLongLabel="@string/static_shortcut_long_label"
    android:shortcutShortLabel="@string/static_shortcut_short_label">
    <intent
      android:action="android.intent.action.VIEW"
      android:targetClass="com.catinean.appshortcutsdemo.StaticShortcutActivity"
      android:targetPackage="com.catinean.appshortcutsdemo" />
  </shortcut>
</shortcuts>

You can see that the root tag of this file is <shortcuts>, which can hold multiple <shortcut> blocks. Each of them, as you may have guessed, represents a static shortcut. Here, the following properties can be set on one shortcut:

  • enabled: As the name states, whether the shortcut is enabled or not. If you decide to disable your static shortcut you could either set this to false or simply remove it from the <shortcuts> set. One use-case where you might want to use this feature is to control which shortcut is disabled by build flavour.
  • icon: The icon shown on the left hand side of the shortcut.
  • shortcutDisabledMessage: this is the string shown to a user if they try to launch a disabled shortcut pinned to their home screen.
  • shortcutLongLabel: This is a longer variant of the shortcut text, shown when the launcher has enough space.
  • shortcutShortLabel: This is a concise description of the shortcut.This field is mandatory. This is probably the shortcut text that most users will see on their home screen.
  • intent: here you define your intent (or more intents) that your shortcut will open upon being tapped

Here’s how this shortcut would appear to a user of your app:

app_shortcut_static


Nice and easy, but if you implement this you may notice that upon pressing back the user is taken back to the home screen. What about instead navigating ‘up’ within the app? To do so, we can add multiple intent tags under the shortcut ones we previously created:

<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
  <shortcut
  ...>
    <intent
      android:action="android.intent.action.MAIN"
      android:targetClass="com.catinean.appshortcutsdemo.MainActivity"
      android:targetPackage="com.catinean.appshortcutsdemo" />
    <intent
      android:action="android.intent.action.VIEW"
      android:targetClass="com.catinean.appshortcutsdemo.StaticShortcutActivity"
      android:targetPackage="com.catinean.appshortcutsdemo" />
  </shortcut>
</shortcuts>

Notice how we added an extra <intent> before the one that we had, pointing to the MainActivity. This will create a back stack of Intents, the last one being the one opened by the shortcut. In our case the back stack looks like MainActivity -> Static ShortcutActivity, so when pressing back the user is taken into the MainActivity:

app_shortcut_static_back_stack

Adding static shortcuts is pretty easy. Let's move on defining some dynamic ones.

Dynamic shortcuts

As their name suggests, dynamic shortcuts can be modified at runtime without the need of re-deploying your app. As you may have guessed, these are not defined through a static resource (shortcuts.xml) like the static ones, but are created in code.

Let's add our first dynamic shortcut! In order to do so, you will have to make use of the ShortcutManager and the ShortcutInfo.Builder. I'll be constructing the first dynamic shortcut in my MainActivity.#onCreate():

@Override
protected void onCreate(Bundle savedInstanceState) {


    ...


    ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);


    ShortcutInfo webShortcut = new ShortcutInfo.Builder(this, "shortcut_web")
            .setShortLabel("novoda.com")
            .setLongLabel("Open novoda.com web site")
            .setIcon(Icon.createWithResource(this, R.drawable.ic_dynamic_shortcut))
            .setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("https://novoda.com")))
            .build();


    shortcutManager.setDynamicShortcuts(Collections.singletonList(webShortcut));
}

In the example above we acquire the shortcutManager and construct a ShortcutInfo. By using the ShortcutInfo.Builder we can set various properties for the shortcut we want to create. All the builder methods we use above correspond to the same properties used for a static shortcut, so I’ll skip e explaining them again. However, one property that is a bit obscure is the id of the shortcut which is defined in the StaticInfo.Builder constructor as second parameter - shortcut_web. In the example above I've defined the Intent being one that will open my website. Finally, I set the dynamic shortcut on the ShortcutManager. Let's see now how our shortcuts look now:

app_shortcut_dynamic_website


Great! Now we have 2 app shortcuts in our app - one static and one dynamic.

Let's add another one that will point to an activity inside the app and see how we can create a back stack for it:

@Override
protected void onCreate(Bundle savedInstanceState) {


    ...


    ShortcutInfo dynamicShortcut = new ShortcutInfo.Builder(this, "shortcut_dynamic")
            .setShortLabel("Dynamic")
            .setLongLabel("Open dynamic shortcut")
            .setIcon(Icon.createWithResource(this, R.drawable.ic_dynamic_shortcut_2))
            .setIntents(
                    new Intent[]{
                            new Intent(Intent.ACTION_MAIN, Uri.EMPTY, this, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK),
                            new Intent(DynamicShortcutActivity.ACTION)
                    })
            .build();


    shortcutManager.setDynamicShortcuts(Arrays.asList(webShortcut, dynamicShortcut));
}

You can see now that we now setIntents() on the builder in order to build a back stack:

  • The first intent corresponds to the MainActivity. We specify a FLAG_ACTIVITY_CLEAR_TASK flag in order to clear any existing tasks related to the app and make the MainActivity the current root activity
  • The second one corresponds to the DynamicShortcutActivity (which is just an empty activity that I created). In order to do so, we need to provide an Intent with a specific action, which is defined as a static String in DynamicShortcutActivity and corresponds to the intent-filter action name defined in AndroidManifest.xml for the same activity:
<activity
      android:name=".DynamicShortcutActivity"
      android:label="Dynamic shortcut activity">
      <intent-filter>
        <action android:name="com.catinean.appshortcutsdemo.OPEN_DYNAMIC_SHORTCUT" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
</activity>

By declaring this array of intents in this order, we ensure that when the user presses back after opening DynamicShortcutActivity through the shortcut we created, the MainActivity will be opened.

Let's see how they look like:

app_shortcut_dynamic_activity

Shortcut ordering

Now that we have 1 static shortcut and 2 dynamic ones, how can we specify a custom order for them? If we take a closer look at the ShortcutInfo.Builder methods, one in particular gives us a clue: setRank(int). By setting a custom rank to a dynamic shortcut we can control the order they appear when revealed: the higher the rank, the most top the shortcut goes.

As an example, say we want shortcut number 2 (novoda.com) to sit at the top. We can dynamically change the ranks of the already added dynamic shortcuts. Let's do this when pressing a button from MainActivity:

findViewById(R.id.main_rank_button).setOnClickListener(new View.OnClickListener() {


      @Override
      public void onClick(View view) {
          ShortcutInfo webShortcut = new ShortcutInfo.Builder(MainActivity.this, "shortcut_web")
                  .setRank(1)
                  .build();


          ShortcutInfo dynamicShortcut = new ShortcutInfo.Builder(MainActivity.this, "shortcut_dynamic")
                  .setRank(0)
                  .build();


          shortcutManager.updateShortcuts(Arrays.asList(webShortcut, dynamicShortcut));
      }
});

In the click listener of the button we create a new ShortcutInfo for each shortcut we have previously added with the same IDs, but now we set a higher rank to the shortcut_web one and a lower one for shortcut_dynamic. Finally, we use the updateShortcuts(List<ShortcutInfo>) method of the ShortcutManager to update the shortcuts with the newly set ranks:

app_shortcut_ranks

You can see from the above gif that the static shortcut sits at the bottom of the list. One thing to note: you cannot change the rank of a static shortcut. They will be shown in the order they're defined in the shortcuts.xml file. Since we have only one static shortcut, it has the default rank of 0 which cannot be changed.

Extra bits

If we take a closer look at the setShortLabel(CharSequence) method of ShortcutInfo.Builder, we can see that it accepts a CharSequence as a parameter. What does this mean? Well, it means that we can play around a little with it as we can attach custom spans to it.

Let's say we want to change its colour to red when pressing the above created button. We can create a SpannableStringBuilder and set to it a ForegroundColorSpan with the desired colour and then pass the spannableStringBuilder as a shortLabel (as SpannableStringBuilder implements CharSequence):

findViewById(R.id.main_rank_button).setOnClickListener(new View.OnClickListener() {


    @Override
    public void onClick(View view) {


        ForegroundColorSpan colorSpan = new ForegroundColorSpan(getResources().getColor(android.R.color.holo_red_dark, getTheme()));
        String label = "novoda.com";
        SpannableStringBuilder colouredLabel = new SpannableStringBuilder(label);
        colouredLabel.setSpan(colorSpan, 0, label.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);


        ShortcutInfo webShortcut = new ShortcutInfo.Builder(MainActivity.this, "shortcut_web")
                .setShortLabel(colouredLabel)
                .setRank(1)
                .build();


        ...
    }
});

app-shortcuts-spans

Wrapping up

  • App Shortcuts are great for exposing actions of your app and bringing back users into your flow
  • They can be static or dynamic
  • Static shortcuts are set in stone once you define them (you can only update them with an app redeploy)
  • Dynamic shortcuts can be changed on the fly
  • You can create a back stack of activities for each shortcut
  • Shortcuts can be reordered, but only in their respective type
  • Static shortcuts will come always at the bottom as they're added first (there's no rank property to be defined on them)
  • The labels of the shortcuts contain a CharSequence so you can manipulate them through spans

You can checkout this blog post's sample app on Github

This is a cross-post from https://catinean.com/2016/10/20/exploring-android-nougat-7-1-app-shortcuts/


Enjoyed this article? There's more...

We send out a small, valuable newsletter with the best stories, app design & development resources every month.

No spam, no giving your data away, unsubscribe anytime.

About Novoda

We plan, design, and develop the world’s most desirable software products. Our team’s expertise helps brands like Sony, Motorola, Tesco, Channel4, BBC, and News Corp build fully customized Android devices or simply make their mobile experiences the best on the market. Since 2008, our full in-house teams work from London, Liverpool, Berlin, Barcelona, and NYC.

Let’s get in contact