Android ShortCuts注意事项

1,772 阅读5分钟

概述

最近在做有关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的删除实现,大致流程如下

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);

最后

欢迎关注公众号,谈谈技术,聊聊人生

qrcode_for_gh_665b92827be4_344.jpg