App Shortcuts 是 Android 7.1 上推出的新功能。借助于这项功能,应用程序可以在 Launcher 中放置一些常用的应用入口以方便用户使用。
App Shortcuts 使用起来像下面这个样子:
每个 Shortcut 可以对应一个或者多个 Intent,它们各自会通过特定的 Intent 来启动你的应用程序,例如:
当一个 Shortcut 包括了多个 Intent 时,用户的一次点击会触发所有这些 Intent,这其中的最后一个 Intent 决定了用户所看到的结果。
使用 App Shortcuts 有两种形式:
目前,每个应用最多可以注册 5 个 Shortcuts,无论是动态形式还是静态形式。
通过动态形式注册的 Shortcut,通常是特定的与用户使用上下文相关的一些动作。这些动作在用户的使用过程中,可能会发生变化。
ShortcutManager 提供了 API 来动态管理 Shortcut,包括:
下面是一段代码示例:
- 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 上。通过添加一个
子元素来并指定定义 Shortcuts 资源文件:
- <meta-data>
- <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 这个资源文件中,添加一个
- <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>
相关代码:
无论是静态注册还是动态注册的 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;
- }
这段代码的主要逻辑包括五个步骤:
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);
- }
- }
顺着这个方法往下看,会看到一系列的调用,如下所示:
最终,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;
- }
这段代码应该还是比较容易理解的,主要逻辑包含三个步骤:
解析的过程就是对 XML 文件每个元素逐个读取的过程,这里我们就不贴这部分代码了。
解析完成之后便会将结果存储在相应的结构中(即上面表格中提到的那些类中)。当下次再次查询的时候,如果包结构没有发生变化,则不必再次解析了。
在系统已经获取到所有包的 Shortcut 信息之后,Launcher 应用只需要通过 ShortcutManager 相应的接口来获取 Shortcut 列表。当用户在桌面图标上长按的时候,显示相应的 Shortcut 信息,当用户点击的时候,根据 Shortcut 中的 Intent 发送即可。
可见,App Shortuct 的实现还是比较简单的。
来源: http://www.tuicool.com/articles/AB3I3qB