Android ShortCuts注意事项

qn4167 3年前
   <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>