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这个资源文件中,添加一个
<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;
}
这段代码的主要逻辑包括五个步骤:
- 通过包名和UserId来获取ShortcutPackage
- 删除已经存在的动态Shortcut
- 添加新的Shortcut
- 调整顺序
- 通知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;
}
这段代码应该还是比较容易理解的,主要逻辑包含三个步骤:
- 解析出所有的Main Activity,即action为“android.intent.action.MAIN”,category为“ android.intent.category.LAUNCHER”的Activity。这一点我们在上文中已经说过了:Shortcut只会配置在Main Activity上
- 遍历所有的Main Activity
- 查看这个Activity有没有配置Metadata,如果有则尝试解析
解析的过程就是对XML文件每个元素逐个读取的过程,这里我们就不贴这部分代码了。
解析完成之后便会将结果存储在相应的结构中(即上面表格中提到的那些类中)。当下次再次查询的时候,如果包结构没有发生变化,则不必再次解析了。
在系统已经获取到所有包的Shortcut信息之后,Launcher应用只需要通过ShortcutManager相应的接口来获取Shortcut列表。当用户在桌面图标上长按的时候,显示相应的Shortcut信息,当用户点击的时候,根据Shortcut中的Intent发送即可。
可见,App Shortuct的实现还是比较简单的。