Android ShortCuts注意事項
概述
最近在做有關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的刪除實現,大致流程如下
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);
來自:http://www.jianshu.com/p/e5323fbc2625