Tuesday 31 January 2017

Implementing Android App Shortcuts

Andrew Orobator:

With every release of Android, there comes a myriad of new features and Android Nougat 7.1 is no different. One of these new features is App Shortcuts. App Shortcuts allow the user to access primary actions in your app straight from the launcher, taking the user deep into your application, by long pressing on your app icon. Users can also pin these shortcuts to the home screen for even quicker access to your app’s primary actions. In this tutorial, we’ll cover how to boost your user’s experience by implementing App Shortcuts in your app.


The first thing you should know about implementing App Shortcuts is that there are two types of shortcuts: static and dynamic. Static shortcuts are generated from an XML file that you create. This means that they can only be updated when the user updates the app. On the other hand, dynamic shortcuts can be created and destroyed at runtime, meaning you can do things like show different shortcuts based on frequency of use. I’ve created a sample app in Kotlin called Konstellations demonstrating the use of both types of shortcuts and the source is available on Github. I’ll be going through the source code of Konstellations as I explain how to implement App Shortcuts, so you can get your hands on a real world example.

Static Shortcuts

The first kind of shortcut we’ll implement is a static shortcut. In your AndroidManifest.xml, we have to add some metadata to your launcher activity, but in practice, any Activity with the intent filter set to the android.intent.action.MAIN action and the android.intent.category.LAUNCHER category will do. In my manifest, I added the <meta-data> tag to MainActivity which is my launcher activity. The name attribute is set to “android.app.shortcuts” and the resource attribute is set to “@xml/shortcuts”.

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

  <application
    android:name=".KonstellationsApplication"
    android:allowBackup="true"
    android:icon="@drawable/app_icon"
    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"/>
      </intent-filter>

      <meta-data
        android:name="android.app.shortcuts"
        android:resource="@xml/shortcuts"/>
     </activity>
     <activity
       android:name=".ConstellationDetailActivity"
       android:parentActivityName=".MainActivity"
       android:theme="@style/DetailTheme">
     </activity>
   </application>

</manifest>

                               Konstellations/app/src/main/AndroidManifest.xml

Now we need to actually make the file where the shortcuts are defined. I created mine as res/xml-v25/shortcuts.xml. Note the resource qualifier; I had to add this so Android Studio would stop yelling at me. It’s a good practice regardless because this feature is only available on API level 25 and above. A copy of my shortcuts.xml is below.

<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
  <shortcut
    android:enabled="true"
    android:icon="@drawable/random_constellation"
    android:shortcutDisabledMessage="@string/shortcut_disabled_message"
    android:shortcutId="random_constellation"
    android:shortcutLongLabel="@string/shortcut_long_label"
    android:shortcutShortLabel="@string/shortcut_short_label">
    <!-- Each one of these intents needs an android:action, even if you don't
        do anything with it. Otherwise you're in for a crash! -->
    <intent
      android:action="android.intent.action.VIEW"
      android:targetClass="com.orobator.konstellations.MainActivity"
      android:targetPackage="com.orobator.konstellations"/>
    <intent
      android:action="RANDOM"
      android:targetClass="com.orobator.konstellations.ConstellationDetailActivity"
      android:targetPackage="com.orobator.konstellations"/>
    <!-- If your shortcut is associated with multiple intents, include them
        here. The last intent in the list is what the user sees when they
        launch this shortcut. The previous intents will be put on the backstack -->
    <!-- You can also specify the category of the shortcut via the <categories>
        tag. As of this writing, the only supported category is
        android.shortcut.conversation, and can be specified using android:name
        like so: -->
    <!-- <categories android:name="android.shortcut.conversation"/> -->

  </shortcut>

</shortcuts>

                                Konstellations/app/src/main/res/xml-v25/shortcuts.xml

As you can see, this needs a root <shortcuts> tag, then you can add a <shortcut> tag to add a shortcut. Don’t go too crazy adding shortcuts though. Android only allows you to have up to five shortcuts, but the documentation strongly suggests providing four (any combination of static and dynamic). Oddly enough, I couldn’t get a fifth shortcut to show up, so it seems their “suggestion” is actually enforced. Add any more than five shortcuts and your app will crash! Now let’s take a closer look at the <shortcut> tag attributes.

android:enabled is a boolean describing whether or not the shortcut is enabled. If disabled, the shortcut will not appear in the list of shortcuts when a user long presses on your app icon in their launcher. In addition, if this shortcut was previously pinned to the user’s home screen, it will appear grayed out, and an error message will appear when the user clicks on it. The default message is “Shortcut isn’t available”, but you can override that by using android:shortcutDisabledMessage and providing a string resource. 

Shortcut_Image_1


You use android:icon to provide an icon for your shortcut. I strongly recommend following the design guidelines here so that you can provide your user with an experience that’s consistent across apps. For the basics though, you’ll want to have a 48dp x 48dp circle that’s Material Grey 100 (or #F5F5F5). Then drop a 24dp x 24dp icon in the middle of that circle and you’ll be golden.
Each shortcut needs a unique android:id so Android can recognize the shortcut.

Both android:shortcutLongLabel and android:shortcutShortLabel are labels shown to the user displaying the name of the shortcut. It’s recommended that the short label be less than or equal to 10 characters, and the long label be less than or equal to 25 characters. Android will pick the appropriate one to display based on the space available, but I’ve noticed that the short label is usually used when placed on the home screen. The long label is usually used whenever there is space in the app shortcut menu.

Shortcut_Image_2

Each <shortcut> tag needs to have at least one <intent> tag specifying the Activity to be launched when the shortcut is clicked. The android:action attribute is mandatory, even if you don’t need an actual action. If you don’t have an action to specify, I’d recommend just using “android.intent.action.VIEW” although the actual string you use doesn’t really matter. It’s okay to put multiple <intent> tags in a <shortcut> tag. Android will handle this by launching the last <intent> specified, and putting the other <intent>s in the backstack. You can also specify the category of a shortcut using the <categories> tag, but as of this writing, the only supported category is “android.shortcut.conversation”. I’d imagine this would only be useful for messaging apps.

Dynamic Shortcuts


Dynamic shortcuts are the second type of shortcuts, and to interact with them (create/destroy/update) you’ll need to use the ShortcutManager. You can get a hold of a ShortcutManager using:
 val shortcutManager = context.getSystemService(ShortcutManager::class.java)
I wrote a small inline helper function to check the API version every time I used the ShortcutManager, so I wouldn’t have to do it all over the app. It’s defined below in ShortcutHelper.kt :

inline fun shortcutAction(action: (ShortcutManager) -> Unit): Unit {
  if (SDK_INT >= N_MR1) {
    val shortcutManager = APP_CONTEXT.getSystemService(ShortcutManager::class.java)
    action(shortcutManager)
  }
}
On application startup in KonstellationApplication.kt, I use the shortcut action to update all the shortcuts. The code for updating shortcuts is as follows:
 /**
 * Sets the new Shortcut List by providing the three most visited
 * constellations
 * */
@TargetApi(N_MR1)
fun updateShortcuts(shortcutManager: ShortcutManager) {
  shortcutManager.dynamicShortcuts =
      Constellation
          .values()
          .sortedWith(compareBy { -getConstellationVisitedCount(it) })
          .map {
            Shortcutinfo.Builder(APP_CONTEXT, it.name)
                .setShortLabel(it.shortName)
                .setLongLabel(it.longName)
                .setIcon(Icon.createWithResource(APP_CONTEXT, R.drawable.shortcut_icon))
                .setIntents(
                    arrayOf(
                        // This intent is used for the back-stack
                        MainActivity.getIntent(APP_CONTEXT),
                        // This intent is what gets initially launched
                       ConstellationDetailActivity.getIntent(APP_CONTEXT, it)
                   )
               )
               .build()
         }.subList(0, 3)
}

Here we’re creating a List<ShortcutInfo> and setting this as the list of dynamic shortcuts that Android will display. First, we get all of the Constellations and sort them by the number of times they were visited in descending order. Then we have a call to map, which is really the bulk of this function. In it we are providing a function that takes a Constellation enum and returns a ShortcutInfo object representing that Constellation. As you can see, the builder API for ShortcutInfo is very similar to the XML used to create static shortcuts. In the constructor, we provide a Context object and the ID for the shortcut. Then we provide a short and a long label, as well as the icon for the shortcut and the Intents that should be launched when it’s clicked. Finally, we only take the first three ShortcutInfo objects to avoid exceeding the limit of shortcuts. There’s a hard limit of five total shortcuts, but Android recommends only showing four. The fourth shortcut comes from the static shortcut we created earlier.
In this example, I’m replacing the list of dynamic shortcuts entirely, but you can also modify the list of dynamic shortcuts via ShortcutManager.addDynamicShortcuts(List)ShortcutManager.updateShortcuts(List), and ShortcutManager.removeDynamicShortcuts(List).

Enabling/Disabling Shortcuts

Now that we know how to make both types of shortcuts, let’s talk about turning them on and off. Like I mentioned before, it’s possible to disable a shortcut if it isn’t relevant anymore, or re-enable it when it does become relevant through the “enabled” attribute. In Konstellations, I do this with a toggle in the overflow menu of ConstellationDetailActivity.kt.

Perseus


@TargetApi(N_MR1)
override fun onOptionsItemSelected(item: MenuItem): Boolean {
  when (item.itemId) {
    enable_shortcut -> shortcutAction {
      it.enableShortcuts(listOf(constellation.name))
      alertUser(shortcut_enabled)
    }
    disable_shortcut -> shortcutAction {
      it.disableShortcuts(listOf(constellation.name))
      alertUser(shortcut_disabled)
    }
    home -> finish()
  }
  return true
}
    Konstellations/app/src/main/kotlin/com/orobator/konstellations/ConstellationDetailActivity.kt

When “Enable Shortcut” is clicked, we call ShortcutManager.enableShortcuts(List) and pass in the list of shortcuts we want to enable. When “Disable Shortcut” is clicked, we call ShortcutManager.disableShortcuts(List) and pass in the shortcuts to disable.

Best Practices


Now that we’ve got the basics down, there’s a couple more things you should know about app shortcuts. Every time a user navigates to a part of your app accessible via shortcut, whether they got there using the shortcut or just navigating through the app, you should call ShortcutManager.reportShortcutUsed(String). This gives launcher apps the information needed to build a prediction model which promotes shortcuts likely to be used. In my onCreate of ConstellationDetailActivity, I make a call to ShortcutHelper.trackShortcutUsed(…) like so:
shortcutAction {
  trackShortcutUsed(it, constellation)
}
 Konstellations/app/src/main/kotlin/com/orobator/konstellations/ConstellationDetailActivity.kt
and the implementation for trackShortcutUsed(...) is below:
@TargetApi(N_MR1)
fun trackShortcutUsed(shortcutManager: ShortcutManager, constellation: Constellation) {
  shortcutManager.reportShortcutUsed(constellation.name)
  val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(APP_CONTEXT)
  val seenCount = sharedPrefs.getInt(constellation.name, 0)
  sharedPrefs
    .edit()
    .putInt(constellation.name, seenCount + 1)
    .apply()
  updateShortcuts(shortcutManager)
}

In this method, I’m calling the aforementioned reportShortcutUsed(String) method. Afterwards, I keep count of how many times the shortcut has been used via SharedPreferences, so that I can directly provide the most used shortcuts to the ShortcutManager. This is the logic that makes my dynamic shortcuts truly dynamic. For your dynamic shortcuts, you should do whatever makes the most sense for your own application. For example, if you were writing a messaging app, you might provide shortcuts for the most recent conversations.

This next best practice is important if you allow the user to back up your app via android:allowBackup=“true” in the manifest. The only shortcuts that will be automatically restored to the user’s homescreen are the static shortcuts that were pinned. Because dynamic shortcuts are not backed up, you’ll have to write logic to re-publish them if a user opens your app on a new device. The Android documentation recommends that you check the number of shortcuts returned by getDynamicShortcuts() each time you launch your app and re-publish dynamic shortcuts as needed. They provide the following Java sample code as an example:

public class MainActivity extends Activity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
        if (shortcutManager.getDynamicShortcuts().size() == 0) {
            // Application restored. Need to re-publish dynamic shortcuts.
            if (shortcutManager.getPinnedShortcuts().size() > 0) {
                // Pinned shortcuts have been restored. Use
                // updateShortcuts(List) to make sure they
                // contain up-to-date information.
            }
        }
    }
    // ...
}

And with that you should be well on your way to implementing Android 7.1’s new feature App Shortcuts! You can find Android’s official documentation for App Shortcuts here. Caveat: as of this writing, 7.1 is still in Preview although it has been released for the Google Pixel, so this link may not be alive forever. Feel free to comment if/when it breaks. Also feel free to leave any questions in the comments, and thanks for reading!

Andrew_Orobator

     Andrew Orobator


I’m an Android developer currently working at American Express. I love tinkering with Android, learning, and finding new ways to grow! You can check out my Medium blog for more Android tips or find me on Twitter @AOrobator.

Referred from : http://www.andevcon.com/news/implementing-android-app-shortcuts




No comments:

Post a Comment