Android ShortCuts注意事项
qn4167
8年前
<p><img src="https://simg.open-open.com/show/3308c603c66b71367e8457d356a259d4.png"></p> <h2>概述</h2> <p>最近在做有关ShortCuts的相关需求,本来以为是个很简单的事情,中途却碰到了一些坑,于是研究了下ShortCuts的生成和删除流程,在这里总结一下分享给大家。</p> <h2>安全问题</h2> <p>你也许会问,ShortCuts还会涉及到安全问题?</p> <p>先不急,这里的安全问题是指的特殊情况,比如点击ShortCuts后跳转到一个Activity,Activity里面有一个Webview控件用于显示指定的Url(假设点击一个叫Test的ShortCuts后,跳转到一个叫webActivity的界面,并且要打开www.test.com这个网址), <strong>先想一想 不往后看</strong> ,你会怎么写</p> <p>你也许会这么写代码</p> <ol> <li> <p>首先,生成快捷方式</p> <pre> <code class="language-java">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);</code></pre> </li> <li> <p>在webActivity里响应请求</p> <pre> <code class="language-java">@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //控件实例化 doViewsInit(); Intent in = getIntent(); String url = in.getStringExtra("url"); webView.loadUrl(URL); }</code></pre> </li> </ol> <p>如果你没有这么写,那么恭喜你,你避过了安全问题,如果你这么写了,那么继续往下面看吧</p> <p>因为响应ShortCuts的Activity必须是 android:exported="true" ,也就是Activity的默认值,不需要显示的配置出来,所以可能很多同学没有注意到。</p> <p>exported="true"是个什么概念呢?就是说 <strong>任何第三方</strong> 的程序都可以访问你这个界面,那么问题就来了.</p> <p>load文件</p> <p>既然刚才说到任何三方都可以调用,那么另外写一个app并且写如下代码也是可以运行的</p> <pre> <code class="language-java">Intent i = new Intent(); i.setClassName("xxx.xxx.xxx", "xxx.xxx.xxx.webActivity"); i.putExtra("url", "http://www.baidu.com/"); startActivity(i);</code></pre> <p>这样,你就可以在自己的app里,隐式调用别人写好的界面,传入自己的参数。</p> <p>你也许又要问了,这打开一个百度有什么好安全不安全的,那么我们换成一个其他的路径,比如: <strong>文件路径</strong></p> <pre> <code class="language-java">i.putExtra("url", ""file:///data/data/xxx.xxx.xxx/shared_prefs/a.xml")");</code></pre> <p>你会发现,webview <strong>加载出</strong> 了这个sp文件,这样就能获取到别人的一些信息了。</p> <p>关于如何获取别人包名和是否使用了webview,手段多种多样,不在这里累述。</p> <p>至于除了加载文件还能有什么操作,欢迎各位补充.</p> <p>规避</p> <p>主要原因是出在 android:exported="true" 上面,因为这个参数是将自己暴露出来,又不能改为false,因为ShortCuts必须得是true。</p> <p>有同学会想,如果ShortCuts跳转的不是一个Activity,而是一个service,在service里面在启动Activity可以不呢? 答案当然是:不可以。 因为跳转对象只有是Activity才会生成ShortCuts。</p> <p>因为webActivity没有办法判断是谁启动了自己,所以唯一的办法就是webActivity就不能直接接受 url 这个参数,而是接受一个类型参数,比如type=1,当拿到这个type=1的情况下,再在webActivity里去app中取对应的url地址,这样就只会认自己app的地址。</p> <h2>无法删除</h2> <p>在网上搜索各种资料,你会发现,让你删除shortcut时,传递的intent必须是和创建时一致的。</p> <p>这当然没有错,但是也 <strong>没有</strong> 全对。</p> <p>但是当你以如下方式创建shortcut时候</p> <pre> <code class="language-java">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);</code></pre> <p>即使删除时使用同样的intent也没办办法删除</p> <pre> <code class="language-java">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);</code></pre> <p>这是为什么呢?!</p> <p>还得来看看ShortCuts的删除实现,大致流程如下</p> <p><img src="https://simg.open-open.com/show/7d057df310a4f1d41b72101f140aebd3.png"></p> <p>shortcuts_flow.png</p> <p>我们来看看launcher中,接受到广播后是如何处理的,如下给出主要函数</p> <p>packages/apps/Launcher3/src/com/android/launcher3/UninstallShortcutReceiver.java</p> <pre> <code class="language-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); ... } ... } }</code></pre> <p>其中这个intent 就是App中传入的EXTRA_SHORTCUT_INTENT,name就是ShortCuts的名字,如果都不为空,则在数据库中查找匹配的数据,这里只是对 <strong>名字</strong> 进行了匹配,匹配不到则无法删除,所以我们刚才的例子,是可以匹配到的.</p> <p>所以问题的关键是,如下条件是否可以通过,如果通过则删除ShortCut</p> <pre> <code class="language-java">if (intent.filterEquals(Intent.parseUri(c.getString(intentIndex), 0))) {...}</code></pre> <p>filterEquals</p> <p>这个方法的作用是,判断2个intent是否完全一致,包括action, type, package等信息</p> <pre> <code class="language-java">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; }</code></pre> <p>所以,网上说的,删除与创建的intent需要完全一致 是正确的.</p> <p>但是上面的例子,2个intent确实是完全一致的, <strong>为什么</strong> 还是会无法删除呢?</p> <p>parseUri</p> <p>说明parseUri方法,在我们传递过来的intent中,添加了一点料,这个料是什么呢?</p> <p>以上面的例子来说,在数据库中 LauncherSettings.Favorites.INTENT 字段下面的值,是这样的</p> <pre> <code class="language-java">#Intent;component=com.example.hly.demo/.MainActivity;end</code></pre> <p>然后通过parseUri方法转换成一个Intent</p> <pre> <code class="language-java">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; }</code></pre> <p>重点来了!!!</p> <p>parseUri返回的是一个intent,而这个intent在实例化的时候却带得有一个 <strong>ACTION_VIEW</strong> 的action</p> <p>这是什么意思?</p> <p>就是说,如果你创建shortcut时的intent中是没有带action信息,launcher不会存入action信息,但是在删除的时候取出来进行匹配的时候,系统会自动给你加上ACTION_VIEW的action,从而导致了 <strong>匹配失败!!</strong></p> <p>但是如果,你创建shortcut的intent是带得有action信息的,在匹配的时候,这个action信息会把系统的ACTION_VIEW这个action <strong>覆盖</strong> ,这样就能和删除时的intent进行匹配了</p> <p>所以刚才的例子如果想正确删除的话,需要加入ACTION_VIEW的action</p> <pre> <code class="language-java">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);</code></pre> <p>来自:http://www.jianshu.com/p/e5323fbc2625</p> <p> </p>