Android 7.1上的App Shortcut功能讲解
fansiyu
8年前
<p>App Shortcuts是Android 7.1上推出的新功能。借助于这项功能,应用程序可以在Launcher中放置一些常用的应用入口以方便用户使用。</p> <p>App Shortcuts使用起来像下面这个样子:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/49af7f17d4f517184fbf44f58518d5dd.png"></p> <p>每个Shortcut可以对应一个或者多个Intent,它们各自会通过特定的Intent来启动你的应用程序,例如:</p> <ul> <li>对于一个地图应用,可以提供一个Shortcut导航用户至某个特定的地点</li> <li>对于一个通信应用,可以提供一个Shortcut来发送消息给好友</li> <li>对于一个视频应用,可以提供一个Shortcut来播放某个电视剧</li> <li>对于一个游戏应用,可以提供一个Shortcut来继续上次的存档</li> </ul> <p>当一个Shortcut包括了多个Intent时,用户的一次点击会触发所有这些Intent,这其中的最后一个Intent决定了用户所看到的结果。</p> <h2>开发者API</h2> <p>使用App Shortcuts有两种形式:</p> <ul> <li>动态形式:在运行时,通过ShortcutManager API来进行注册。通过这种方式,你可以在运行时,动态的发布,更新和删除Shortcut。</li> <li>静态形式:在APK中包含一个资源文件来描述Shortcut。这种注册方法将导致:如果你要更新Shortcut,你必须更新整个应用程序。</li> </ul> <p>目前,每个应用最多可以注册5个Shortcuts,无论是动态形式还是静态形式。</p> <h2>动态形式</h2> <p>通过动态形式注册的Shortcut,通常是特定的与用户使用上下文相关的一些动作。这些动作在用户的使用过程中,可能会发生变化。</p> <p>ShortcutManager提供了API来动态管理Shortcut,包括:</p> <ul> <li>通过setDynamicShortcuts() 来更新整个动态Shortcut列表,或者通过addDynamicShortcuts() 来向已经存在的列表中添加新的条目</li> <li>通过updateShortcuts() 来进行更新</li> <li>通过removeDynamicShortcuts()来删除指定的Shortcuts,或者通过removeAllDynamicShortcuts()来删除所有动态Shortcuts</li> </ul> <p>下面是一段代码示例:</p> <pre> <code class="language-java">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));</code></pre> <h2>静态形式</h2> <p>静态Shortcut应当提供应用程序中比较通用的一些动作,例如:发送短信,设置闹钟等等。</p> <p>开发者通过下面的方式来设置静态Shortcuts:</p> <p>App Shortcuts是在Launcher上显示在应用程序的入口上的,因此需要设置在action为“android.intent.action.MAIN”,category为“ android.intent.category.LAUNCHER”的Activity上。通过添加一个 <meta-data> 子元素来并指定定义Shortcuts资源文件:</p> <pre> <code class="language-java"><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></code></pre> <p>在res/xml/shortcuts.xml这个资源文件中,添加一个 根元素,根元素中包含若干个 子元素,每个 描述了一个Shortcut,其中包含:icon,description labels以及启动应用的Intent。</p> <pre> <code class="language-java"><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></code></pre> <h2>内部实现</h2> <p>相关代码:</p> <ul> <li>/frameworks/base/core/java/android/content/pm/</li> <li>/frameworks/base/services/core/java/com/android/server/pm/</li> </ul> <p>无论是静态注册还是动态注册的Shortcut,最终都是通过ShortcutInfo这个类来描述的。我们可以顺着ShortcutManager和ShortcutInfo来了解相关实现。</p> <p>ShortcutManager类开始的一段代码如下:</p> <pre> <code class="language-java">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; } ... }</code></pre> <p>细心的读者会发现,ShortcutManager构造函数上面有一个“@hide”注解。</p> <p>如果你浏览过过Android Framework中的代码,就会发现很多的方法上面都有这个注解。这个注解的作用是:表示这个接口是系统内部实现所用,开发者无法直接调用。即:即便ShortcutManager中有这个构造方法,但我们在开发应用程序时也是无法调用的。相应的,Framework提供了 getSystemService这样的接口来让我们获取需要的服务。</p> <p>我们看到,ShortcutManager的构造函数需要一个Context对象和一个IShortcutService。这个Context对象便是我们调用getSystemService(ShortcutManager.class)的Context(例如Activity),这个对象对应了调用者身份。而IShortcutService对象是什么呢?看过Binder相关内容的读者可能很快就会想到:这是一个Binder服务的接口对象。</p> <p>是的,没错!在之前的讲解中,我们已经提到过:系统服务运行在专门的系统进程中,许多Framework层的系统服务都是通过Binder实现的,然后通过IPC的形式来暴露接口以供外部使用,IShortcutService也是一样。</p> <p>ShortcutManager对应的实现是ShortcutService。</p> <p>其代码位于:/frameworks/base/services/core/java/com/android/server/pm 目录下。</p> <p>下面我来详细看一下,两种方式注册Shortcut各是如何实现的。</p> <h2>动态注册</h2> <p>上文中我们看到,我们是通过ShortcutManager.setDynamicShortcuts来设置动态Shorcut的,那么对应的实现自然是ShortcutService.setDynamicShortcuts方法,该方法主要代码如下:</p> <pre> <code class="language-java">@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; }</code></pre> <p>这段代码的主要逻辑包括五个步骤:</p> <ol> <li>通过包名和UserId来获取ShortcutPackage</li> <li>删除已经存在的动态Shortcut</li> <li>添加新的Shortcut</li> <li>调整顺序</li> <li>通知Launcher Shortcut发生了变化</li> </ol> <p>Android 自4.2以来就开始支持多用户功能,同一时间可能有多个用户在同时运行着。而UserId便是用户的标识。在默认情况下,如果设备中没有启用多用户功能,则默认的UserId是0,对应的用户是设备的Owner。</p> <p>这里我们看到了一个叫做ShortcutPackage的类。如果你顺着这段代码深入看的话,会发现这里还会牵涉到更多与Shortcut相关的类。下表是对它们的集中说明:</p> <table> <thead> <tr> <th>类名</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>ShortcutPackageInfo</td> <td>ShortcutManager用来进行备份和恢复使用</td> </tr> <tr> <td>ShortcutPackageItem</td> <td>Shortcut包条目</td> </tr> <tr> <td>ShortcutPackage</td> <td>ShortcutPackageItem的子类,包含了一个包里面的所有Shortcut</td> </tr> <tr> <td>ShortcutUser</td> <td>包含了一个用户的所有Shortcut</td> </tr> <tr> <td>ShortcutParser</td> <td>对Shortcut XML配置文件的解析类</td> </tr> </tbody> </table> <p>系统会对所有应用的Shortcut进行备份,备份的格式是XML文件。这些文件会按用户分开目录存储。设备Owner的Shortcut备份文件位于:/data/system_ce/0/shortcut_service/ 目录下。</p> <h2>静态注册</h2> <p>下面我们来看一下通过Manifest以静态形式注册的Shortcut是如何管理的。</p> <p>下面这个方法用来获取在Manifest中注册的Shortcut列表:</p> <pre> <code class="language-java">@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); } }</code></pre> <p>顺着这个方法往下看,会看到一系列的调用,如下所示:</p> <ul> <li>ShortcutService.getManifestShortcuts =></li> <li>ShortcutService.getShortcutsWithQueryLocked =></li> <li>ShortcutService.getPackageShortcutsForPublisherLocked =></li> <li>ShortcutService.getUserShortcutsLocked =></li> <li>ShortcutUser.getPackageShortcuts =></li> <li>ShortcutUser.onCalledByPublisher =></li> <li>ShortcutUser.rescanPackageIfNeeded =></li> <li>ShortcutPackage.rescanPackageIfNeeded =></li> <li>ShortcutParser.parseShortcuts =></li> </ul> <p>最终,ShortcutParser.parseShortcuts是解析开发者配置的Shortcut XML文件的实现,该方法代码如下:</p> <pre> <code class="language-java">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; }</code></pre> <p>这段代码应该还是比较容易理解的,主要逻辑包含三个步骤:</p> <ol> <li>解析出所有的Main Activity,即action为“android.intent.action.MAIN”,category为“ android.intent.category.LAUNCHER”的Activity。这一点我们在上文中已经说过了:Shortcut只会配置在Main Activity上</li> <li>遍历所有的Main Activity</li> <li>查看这个Activity有没有配置Metadata,如果有则尝试解析</li> </ol> <p>解析的过程就是对XML文件每个元素逐个读取的过程,这里我们就不贴这部分代码了。</p> <p>解析完成之后便会将结果存储在相应的结构中(即上面表格中提到的那些类中)。当下次再次查询的时候,如果包结构没有发生变化,则不必再次解析了。</p> <p>在系统已经获取到所有包的Shortcut信息之后,Launcher应用只需要通过ShortcutManager相应的接口来获取Shortcut列表。当用户在桌面图标上长按的时候,显示相应的Shortcut信息,当用户点击的时候,根据Shortcut中的Intent发送即可。</p> <p>可见,App Shortuct的实现还是比较简单的。</p> <p> </p> <p>来自:http://qiangbo.space/2017-04-16/AndroidAnatomy_AppShortcut/</p> <p> </p>