Android 7.1上的App Shortcut功能讲解

Posted on Apr 16, 2017


App Shortcuts是Android 7.1上推出的新功能。借助于这项功能,应用程序可以在Launcher中放置一些常用的应用入口以方便用户使用。

App Shortcuts使用起来像下面这个样子:

每个Shortcut可以对应一个或者多个Intent,它们各自会通过特定的Intent来启动你的应用程序,例如:

  • 对于一个地图应用,可以提供一个Shortcut导航用户至某个特定的地点
  • 对于一个通信应用,可以提供一个Shortcut来发送消息给好友
  • 对于一个视频应用,可以提供一个Shortcut来播放某个电视剧
  • 对于一个游戏应用,可以提供一个Shortcut来继续上次的存档

当一个Shortcut包括了多个Intent时,用户的一次点击会触发所有这些Intent,这其中的最后一个Intent决定了用户所看到的结果。

开发者API

使用App Shortcuts有两种形式:

  • 动态形式:在运行时,通过ShortcutManager API来进行注册。通过这种方式,你可以在运行时,动态的发布,更新和删除Shortcut。
  • 静态形式:在APK中包含一个资源文件来描述Shortcut。这种注册方法将导致:如果你要更新Shortcut,你必须更新整个应用程序。

目前,每个应用最多可以注册5个Shortcuts,无论是动态形式还是静态形式。

动态形式

通过动态形式注册的Shortcut,通常是特定的与用户使用上下文相关的一些动作。这些动作在用户的使用过程中,可能会发生变化。

ShortcutManager提供了API来动态管理Shortcut,包括:

  • 通过setDynamicShortcuts() 来更新整个动态Shortcut列表,或者通过addDynamicShortcuts() 来向已经存在的列表中添加新的条目
  • 通过updateShortcuts() 来进行更新
  • 通过removeDynamicShortcuts()来删除指定的Shortcuts,或者通过removeAllDynamicShortcuts()来删除所有动态Shortcuts

下面是一段代码示例:

ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);

ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "id1")
    .setShortLabel("Web site")
    .setLongLabel("Open the web site")
    .setIcon(Icon.createWithResource(context, R.drawable.icon_website))
    .setIntent(new Intent(Intent.ACTION_VIEW,
                   Uri.parse("https://www.mysite.example.com/")))
    .build();

shortcutManager.setDynamicShortcuts(Arrays.asList(shortcut));

静态形式

静态Shortcut应当提供应用程序中比较通用的一些动作,例如:发送短信,设置闹钟等等。

开发者通过下面的方式来设置静态Shortcuts:

App Shortcuts是在Launcher上显示在应用程序的入口上的,因此需要设置在action为“android.intent.action.MAIN”,category为“ android.intent.category.LAUNCHER”的Activity上。通过添加一个<meta-data> 子元素来并指定定义Shortcuts资源文件:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.myapplication">
  <application>
    <activity android:name="Main">
      <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>
  </application>
</manifest>

在res/xml/shortcuts.xml这个资源文件中,添加一个根元素,根元素中包含若干个子元素,每个描述了一个Shortcut,其中包含:icon,description labels以及启动应用的Intent。

<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
  <shortcut
    android:shortcutId="compose"
    android:enabled="true"
    android:icon="@drawable/compose_icon"
    android:shortcutShortLabel="@string/compose_shortcut_short_label1"
    android:shortcutLongLabel="@string/compose_shortcut_long_label1"
    android:shortcutDisabledMessage="@string/compose_disabled_message1">
    <intent
      android:action="android.intent.action.VIEW"
      android:targetPackage="com.example.myapplication"
      android:targetClass="com.example.myapplication.ComposeActivity" />
    <categories android:name="android.shortcut.conversation" />
  </shortcut>
  <!-- Specify more shortcuts here. -->
</shortcuts>

内部实现

相关代码:

  • /frameworks/base/core/java/android/content/pm/
  • /frameworks/base/services/core/java/com/android/server/pm/

无论是静态注册还是动态注册的Shortcut,最终都是通过ShortcutInfo这个类来描述的。我们可以顺着ShortcutManager和ShortcutInfo来了解相关实现。

ShortcutManager类开始的一段代码如下:

public class ShortcutManager {
    private static final String TAG = "ShortcutManager";

    private final Context mContext;
    private final IShortcutService mService;

    /**
     * @hide
     */
    public ShortcutManager(Context context, IShortcutService service) {
        mContext = context;
        mService = service;
    }
    
    ...
}

细心的读者会发现,ShortcutManager构造函数上面有一个“@hide”注解。

如果你浏览过过Android Framework中的代码,就会发现很多的方法上面都有这个注解。这个注解的作用是:表示这个接口是系统内部实现所用,开发者无法直接调用。即:即便ShortcutManager中有这个构造方法,但我们在开发应用程序时也是无法调用的。相应的,Framework提供了 getSystemService这样的接口来让我们获取需要的服务。

我们看到,ShortcutManager的构造函数需要一个Context对象和一个IShortcutService。这个Context对象便是我们调用getSystemService(ShortcutManager.class)的Context(例如Activity),这个对象对应了调用者身份。而IShortcutService对象是什么呢?看过Binder相关内容的读者可能很快就会想到:这是一个Binder服务的接口对象。

是的,没错!在之前的讲解中,我们已经提到过:系统服务运行在专门的系统进程中,许多Framework层的系统服务都是通过Binder实现的,然后通过IPC的形式来暴露接口以供外部使用,IShortcutService也是一样。

ShortcutManager对应的实现是ShortcutService。

其代码位于:/frameworks/base/services/core/java/com/android/server/pm 目录下。

下面我来详细看一下,两种方式注册Shortcut各是如何实现的。

动态注册

上文中我们看到,我们是通过ShortcutManager.setDynamicShortcuts来设置动态Shorcut的,那么对应的实现自然是ShortcutService.setDynamicShortcuts方法,该方法主要代码如下:

@Override
public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList,
       @UserIdInt int userId) {
   verifyCaller(packageName, userId); 
   final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
   final int size = newShortcuts.size();
   synchronized (mLock) {
       throwIfUserLockedL(userId);
       final ShortcutPackage ps = getPackageShortcutsForPublisherLocked(packageName, userId); 
       ps.ensureImmutableShortcutsNotIncluded(newShortcuts);
       fillInDefaultActivity(newShortcuts);
       ps.enforceShortcutCountsBeforeOperation(newShortcuts, OPERATION_SET);
       // Throttling.
       if (!ps.tryApiCall()) {
           return false;
       }
       // Initialize the implicit ranks for ShortcutPackage.adjustRanks().
       ps.clearAllImplicitRanks();
       assignImplicitRanks(newShortcuts);
       for (int i = 0; i < size; i++) {
           fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false);
       }
       // First, remove all un-pinned; dynamic shortcuts
       ps.deleteAllDynamicShortcuts(); 
       // Then, add/update all.  We need to make sure to take over "pinned" flag.
       for (int i = 0; i < size; i++) { 
           final ShortcutInfo newShortcut = newShortcuts.get(i);
           ps.addOrUpdateDynamicShortcut(newShortcut);
       } 
       // Lastly, adjust the ranks.
       ps.adjustRanks(); 
   }
   packageShortcutsChanged(packageName, userId); 
   verifyStates();
   return true;
}

这段代码的主要逻辑包括五个步骤:

  1. 通过包名和UserId来获取ShortcutPackage
  2. 删除已经存在的动态Shortcut
  3. 添加新的Shortcut
  4. 调整顺序
  5. 通知Launcher Shortcut发生了变化

Android 自4.2以来就开始支持多用户功能,同一时间可能有多个用户在同时运行着。而UserId便是用户的标识。在默认情况下,如果设备中没有启用多用户功能,则默认的UserId是0,对应的用户是设备的Owner。

这里我们看到了一个叫做ShortcutPackage的类。如果你顺着这段代码深入看的话,会发现这里还会牵涉到更多与Shortcut相关的类。下表是对它们的集中说明:

类名 说明
ShortcutPackageInfo ShortcutManager用来进行备份和恢复使用
ShortcutPackageItem Shortcut包条目
ShortcutPackage ShortcutPackageItem的子类,包含了一个包里面的所有Shortcut
ShortcutUser 包含了一个用户的所有Shortcut
ShortcutParser 对Shortcut XML配置文件的解析类

系统会对所有应用的Shortcut进行备份,备份的格式是XML文件。这些文件会按用户分开目录存储。设备Owner的Shortcut备份文件位于:/data/system_ce/0/shortcut_service/ 目录下。

静态注册

下面我们来看一下通过Manifest以静态形式注册的Shortcut是如何管理的。

下面这个方法用来获取在Manifest中注册的Shortcut列表:

@Override
public ParceledListSlice<ShortcutInfo> getManifestShortcuts(String packageName,
       @UserIdInt int userId) {
   verifyCaller(packageName, userId);

   synchronized (mLock) {
       throwIfUserLockedL(userId);

       return getShortcutsWithQueryLocked(
               packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
               ShortcutInfo::isManifestShortcut);
   }
}

顺着这个方法往下看,会看到一系列的调用,如下所示:

  • ShortcutService.getManifestShortcuts =>
  • ShortcutService.getShortcutsWithQueryLocked =>
  • ShortcutService.getPackageShortcutsForPublisherLocked =>
  • ShortcutService.getUserShortcutsLocked =>
  • ShortcutUser.getPackageShortcuts =>
  • ShortcutUser.onCalledByPublisher =>
  • ShortcutUser.rescanPackageIfNeeded =>
  • ShortcutPackage.rescanPackageIfNeeded =>
  • ShortcutParser.parseShortcuts =>

最终,ShortcutParser.parseShortcuts是解析开发者配置的Shortcut XML文件的实现,该方法代码如下:

public static List<ShortcutInfo> parseShortcuts(ShortcutService service,
       String packageName, @UserIdInt int userId) throws IOException, XmlPullParserException {
   if (ShortcutService.DEBUG) {
       Slog.d(TAG, String.format("Scanning package %s for manifest shortcuts on user %d",
               packageName, userId));
   }
   final List<ResolveInfo> activities = service.injectGetMainActivities(packageName, userId); 
   if (activities == null || activities.size() == 0) {
       return null;
   }

   List<ShortcutInfo> result = null;

   try {
       final int size = activities.size();
       for (int i = 0; i < size; i++) { 
           final ActivityInfo activityInfoNoMetadata = activities.get(i).activityInfo;
           if (activityInfoNoMetadata == null) {
               continue;
           }

           final ActivityInfo activityInfoWithMetadata =
                   service.getActivityInfoWithMetadata(
                   activityInfoNoMetadata.getComponentName(), userId);
           if (activityInfoWithMetadata != null) {
               result = parseShortcutsOneFile( 
                       service, activityInfoWithMetadata, packageName, userId, result);
           }
       }
   } catch (RuntimeException e) {
       // Resource ID mismatch may cause various runtime exceptions when parsing XMLs,
       // But we don't crash the device, so just swallow them.
       service.wtf(
               "Exception caught while parsing shortcut XML for package=" + packageName, e);
       return null;
   }
   return result;
}

这段代码应该还是比较容易理解的,主要逻辑包含三个步骤:

  1. 解析出所有的Main Activity,即action为“android.intent.action.MAIN”,category为“ android.intent.category.LAUNCHER”的Activity。这一点我们在上文中已经说过了:Shortcut只会配置在Main Activity上
  2. 遍历所有的Main Activity
  3. 查看这个Activity有没有配置Metadata,如果有则尝试解析

解析的过程就是对XML文件每个元素逐个读取的过程,这里我们就不贴这部分代码了。

解析完成之后便会将结果存储在相应的结构中(即上面表格中提到的那些类中)。当下次再次查询的时候,如果包结构没有发生变化,则不必再次解析了。

在系统已经获取到所有包的Shortcut信息之后,Launcher应用只需要通过ShortcutManager相应的接口来获取Shortcut列表。当用户在桌面图标上长按的时候,显示相应的Shortcut信息,当用户点击的时候,根据Shortcut中的Intent发送即可。

可见,App Shortuct的实现还是比较简单的。


 Contents