Android 7.1上的App Shortcut功能講解

fansiyu 7年前發布 | 24K 次閱讀 安卓開發 Android開發 移動開發

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的實現還是比較簡單的。

 

來自:http://qiangbo.space/2017-04-16/AndroidAnatomy_AppShortcut/

 

 本文由用戶 fansiyu 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!