概述
最近在做有關ShortCuts的相關需求,本來以為是個很簡單的事情,中途卻碰到了一些坑,於是研究了下ShortCuts的生成和刪除流程,在這裡總結一下分享給大家。
安全問題
你也許會問,ShortCuts還會涉及到安全問題?
先不急,這裡的安全問題是指的特殊情況,比如點選ShortCuts後跳轉到一個Activity,Activity裡面有一個Webview控制元件用於顯示指定的Url(假設點選一個叫Test的ShortCuts後,跳轉到一個叫webActivity的介面,並且要開啟www.test.com這個網址),先想一想 不往後看,你會怎麼寫
你也許會這麼寫程式碼
首先,生成快捷方式
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);複製程式碼
在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的刪除實現,大致流程如下
我們來看看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);複製程式碼
最後
歡迎關注公眾號,談談技術,聊聊人生