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>