Android ShortCuts注意事項

Hly_Coder發表於2016-12-16

概述

最近在做有關ShortCuts的相關需求,本來以為是個很簡單的事情,中途卻碰到了一些坑,於是研究了下ShortCuts的生成和刪除流程,在這裡總結一下分享給大家。

安全問題

你也許會問,ShortCuts還會涉及到安全問題?
先不急,這裡的安全問題是指的特殊情況,比如點選ShortCuts後跳轉到一個Activity,Activity裡面有一個Webview控制元件用於顯示指定的Url(假設點選一個叫Test的ShortCuts後,跳轉到一個叫webActivity的介面,並且要開啟www.test.com這個網址),先想一想 不往後看,你會怎麼寫

你也許會這麼寫程式碼

  1. 首先,生成快捷方式

    Intent shortcut = new Intent("com.android.launcher.action.INSTALL_SHORTCUT");
    shortcut.putExtra(Intent.EXTRA_SHORTCUT_NAME, "Test"); 
    
    Intent.ShortcutIconResource iconRes
              = Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher);
    shortcut.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconRes);
    
    Intent action = new Intent(this, WebActivity.class);
    action.putExtra("url", "www.test.com");
    shortcut.putExtra(Intent.EXTRA_SHORTCUT_INTENT, action);
    sendBroadcast(shortcut);複製程式碼
  2. 在webActivity裡響應請求

     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         //控制元件例項化
         doViewsInit();
         Intent in  = getIntent();
         String url = in.getStringExtra("url");
         webView.loadUrl(URL); 
     }複製程式碼

如果你沒有這麼寫,那麼恭喜你,你避過了安全問題,如果你這麼寫了,那麼繼續往下面看吧
因為響應ShortCuts的Activity必須是android:exported="true",也就是Activity的預設值,不需要顯示的配置出來,所以可能很多同學沒有注意到。
exported="true"是個什麼概念呢?就是說任何第三方的程式都可以訪問你這個介面,那麼問題就來了.

load檔案

既然剛才說到任何三方都可以呼叫,那麼另外寫一個app並且寫如下程式碼也是可以執行的

  Intent i = new Intent();
  i.setClassName("xxx.xxx.xxx",  "xxx.xxx.xxx.webActivity");
  i.putExtra("url", "http://www.baidu.com/");
  startActivity(i);複製程式碼

這樣,你就可以在自己的app裡,隱式呼叫別人寫好的介面,傳入自己的引數。
你也許又要問了,這開啟一個百度有什麼好安全不安全的,那麼我們換成一個其他的路徑,比如:檔案路徑

 i.putExtra("url", 
        ""file:///data/data/xxx.xxx.xxx/shared_prefs/a.xml")");複製程式碼

你會發現,webview載入出了這個sp檔案,這樣就能獲取到別人的一些資訊了。

關於如何獲取別人包名和是否使用了webview,手段多種多樣,不在這裡累述。
至於除了載入檔案還能有什麼操作,歡迎各位補充.

規避

主要原因是出在`android:exported="true"上面,因為這個引數是將自己暴露出來,又不能改為false,因為ShortCuts必須得是true。
有同學會想,如果ShortCuts跳轉的不是一個Activity,而是一個service,在service裡面在啟動Activity可以不呢? 答案當然是:不可以。 因為跳轉物件只有是Activity才會生成ShortCuts。
因為webActivity沒有辦法判斷是誰啟動了自己,所以唯一的辦法就是webActivity就不能直接接受url這個引數,而是接受一個型別引數,比如type=1,當拿到這個type=1的情況下,再在webActivity裡去app中取對應的url地址,這樣就只會認自己app的地址。

無法刪除

在網上搜尋各種資料,你會發現,讓你刪除shortcut時,傳遞的intent必須是和建立時一致的。
這當然沒有錯,但是也沒有全對。
但是當你以如下方式建立shortcut時候

 Intent shortcut = new Intent("com.android.launcher.action.INSTALL_SHORTCUT");
 shortcut.putExtra(Intent.EXTRA_SHORTCUT_NAME, "123");
 Intent.ShortcutIconResource iconRes = 
        Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher);
 shortcut.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconRes);

 //點選後響應的intent沒有action引數
 Intent action = new Intent(this, MainActivity.class);

 shortcut.putExtra(Intent.EXTRA_SHORTCUT_INTENT, action);
 sendBroadcast(shortcut);複製程式碼

即使刪除時使用同樣的intent也沒辦辦法刪除

Intent remove = new Intent("com.android.launcher.action.UNINSTALL_SHORTCUT");
remove.putExtra(Intent.EXTRA_SHORTCUT_NAME, "123");

Intent action2 = new Intent(this, MainActivity.class);

remove.putExtra(Intent.EXTRA_SHORTCUT_INTENT, action2);
sendBroadcast(remove);複製程式碼

這是為什麼呢?!
還得來看看ShortCuts的刪除實現,大致流程如下

Android ShortCuts注意事項
shortcuts_flow.png

我們來看看launcher中,接受到廣播後是如何處理的,如下給出主要函式
packages/apps/Launcher3/src/com/android/launcher3/UninstallShortcutReceiver.java

private static void removeShortcut(Context context, Intent data) {
    Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
    String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
    ...
    if (intent != null && name != null) {
        final ContentResolver cr = context.getContentResolver();
        Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI,
          new String[] { LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT },
          LauncherSettings.Favorites.TITLE + "=?", new String[] { name }, null);

        final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
        ...
        while (c.moveToNext()) {
            ...
            if (intent.filterEquals(Intent.parseUri(c.getString(intentIndex), 0))) {
                ...
                cr.delete(uri, null, null);
                ...
            }
            ...
       }
}複製程式碼

其中這個intent 就是App中傳入的EXTRA_SHORTCUT_INTENT,name就是ShortCuts的名字,如果都不為空,則在資料庫中查詢匹配的資料,這裡只是對名字進行了匹配,匹配不到則無法刪除,所以我們剛才的例子,是可以匹配到的.
所以問題的關鍵是,如下條件是否可以通過,如果通過則刪除ShortCut

if (intent.filterEquals(Intent.parseUri(c.getString(intentIndex), 0))) {...}複製程式碼

filterEquals

這個方法的作用是,判斷2個intent是否完全一致,包括action, type, package等資訊

    public boolean filterEquals(Intent other) {
        if (other == null) {
            return false;
        }
        if (!Objects.equals(this.mAction, other.mAction)) return false;
        if (!Objects.equals(this.mData, other.mData)) return false;
        if (!Objects.equals(this.mType, other.mType)) return false;
        if (!Objects.equals(this.mPackage, other.mPackage)) return false;
        if (!Objects.equals(this.mComponent, other.mComponent)) return false;
        if (!Objects.equals(this.mCategories, other.mCategories)) return false;

        return true;
    }複製程式碼

所以,網上說的,刪除與建立的intent需要完全一致 是正確的.
但是上面的例子,2個intent確實是完全一致的,為什麼還是會無法刪除呢?

parseUri

說明parseUri方法,在我們傳遞過來的intent中,新增了一點料,這個料是什麼呢?

以上面的例子來說,在資料庫中LauncherSettings.Favorites.INTENT欄位下面的值,是這樣的

#Intent;component=com.example.hly.demo/.MainActivity;end複製程式碼

然後通過parseUri方法轉換成一個Intent

public static Intent parseUri(String uri, int flags) throws URISyntaxException {
    ...
     // new format
    Intent intent = new Intent(ACTION_VIEW);
    Intent baseIntent = intent;
    ...
     // action
    if (uri.startsWith("action=", i)) {
        intent.setAction(value);
    }
    ...
    return intent;
}複製程式碼

重點來了!!!
parseUri返回的是一個intent,而這個intent在例項化的時候卻帶得有一個ACTION_VIEW的action
這是什麼意思?
就是說,如果你建立shortcut時的intent中是沒有帶action資訊,launcher不會存入action資訊,但是在刪除的時候取出來進行匹配的時候,系統會自動給你加上ACTION_VIEW的action,從而導致了匹配失敗!!
但是如果,你建立shortcut的intent是帶得有action資訊的,在匹配的時候,這個action資訊會把系統的ACTION_VIEW這個action覆蓋,這樣就能和刪除時的intent進行匹配了

所以剛才的例子如果想正確刪除的話,需要加入ACTION_VIEW的action

Intent remove = new Intent("com.android.launcher.action.UNINSTALL_SHORTCUT");
remove.putExtra(Intent.EXTRA_SHORTCUT_NAME, "123");

Intent action2 = new Intent(this, MainActivity.class);
//重點
action2.setAction(Intent .ACTION_VIEW);.

remove.putExtra(Intent.EXTRA_SHORTCUT_INTENT, action2);
sendBroadcast(remove);複製程式碼

最後

歡迎關注公眾號,談談技術,聊聊人生

Android ShortCuts注意事項
qrcode_for_gh_665b92827be4_344.jpg

相關文章