Android 7.1上的App Shortcut功能講解
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;
}
這段代碼的主要邏輯包括五個步驟:
- 通過包名和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的實現還是比較簡單的。
來自:http://qiangbo.space/2017-04-16/AndroidAnatomy_AppShortcut/