android browser支持支持Intent Scheme URL语法的可以在wrap页面加载或点击时,通过特定的intent uri链接可以打开对应app页面,例如
<a href="intent://whatsapp/#Intent;scheme=myapp;package=com.xiaoyu.myapp;S.help_url=http://Fzxing.org;end">what can I do</a>
通过android系统解析查找符合规则的Activity后启动对应activity,如果未找到匹配Activity则browser进行处理。
配置
<a href="intent://whatsapp/#Intent;scheme=myapp;package=com.xiaoyu.myapp;S.help_url=http://Fzxing.org;end">what can I do</a> //这样如果没有对应应用,该链接就会跳转到S.help_url指定的url上 <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /><!-- 定义浏览器类型,有URL需要处理时会过滤 --> <data android:scheme="myapp" android:host="whatsapp" android:path="/" /><!-- 打开以whatsapp协议的URL,这个自己定义 --> </intent-filter>
URI的解析与生成:
在Intent 类内部有个parseUri(String uri, int flags)方法用来解析Uri生成Intent返回,下面主要分析该方法:
在分析该解析方法时候,首先我们需要理解intent uri规则,在intent类里规则串如下,
android-app://com.example.app/<br />#Intent;action=com.example.MY_ACTION;<br />i.some_int=100;S.some_str=hello;end
根据此规则,看出主要分为三个部分:
Header: android-app://android app为判断是否是android 应用
Action: com.example.MY_ACTION //组件的action内容
Package: com.example.app //包名启动组件使用
Extra: some_int=(int)100,some_str=(String)hello //传递的数据,i代表int,S代表String ,c代表char等基本类型
除了这几部分内容也包含intent内的其它内容,如categry,type,component,selector等
在简单分析理解Uri规则串后,再来分析parseUri就很好理解了,下面看一下主要代码
public static Intent parseUri(String uri, int flags) throws URISyntaxException { int i = 0; try { final boolean androidApp = uri.startsWith("android-app:");//判断开头是否为android, // Validate intent scheme if requested. if ((flags&(URI_INTENT_SCHEME|URI_ANDROID_APP_SCHEME)) != 0) { if (!uri.startsWith("intent:") && !androidApp) {//是否为intent Uri Intent intent = new Intent(ACTION_VIEW); try { intent.setData(Uri.parse(uri)); } catch (IllegalArgumentException e) { throw new URISyntaxException(uri, e.getMessage()); } return intent; } } i = uri.lastIndexOf("#");//查找标记位,开始解析 // simple case if (i == -1) { if (!androidApp) { return new Intent(ACTION_VIEW, Uri.parse(uri)); } // old format Intent URI } else if (!uri.startsWith("#Intent;", i)) { if (!androidApp) { return getIntentOld(uri, flags); } else { i = -1; } } // new format Intent intent = new Intent(ACTION_VIEW); Intent baseIntent = intent; boolean explicitAction = false; boolean inSelector = false; // fetch data part, if present String scheme = null; String data; if (i >= 0) { data = uri.substring(0, i); i += 8; // length of "#Intent;" } else { data = uri; } // loop over contents of Intent, all name=value; while (i >= 0 && !uri.startsWith("end", i)) {//按类别分离循环处理(解析action,categry,type,extra data) int eq = uri.indexOf('=', i); if (eq < 0) eq = i-1; int semi = uri.indexOf(';', i); String value = eq < semi ? Uri.decode(uri.substring(eq + 1, semi)) : ""; // action if (uri.startsWith("action=", i)) { intent.setAction(value); if (!inSelector) { explicitAction = true; } } // categories else if (uri.startsWith("category=", i)) { intent.addCategory(value); } // type else if (uri.startsWith("type=", i)) { intent.mType = value; } // launch flags else if (uri.startsWith("launchFlags=", i)) { intent.mFlags = Integer.decode(value).intValue(); if ((flags& URI_ALLOW_UNSAFE) == 0) { intent.mFlags &= ~IMMUTABLE_FLAGS; } } // package else if (uri.startsWith("package=", i)) { intent.mPackage = value; } // component else if (uri.startsWith("component=", i)) { intent.mComponent = ComponentName.unflattenFromString(value); } // scheme else if (uri.startsWith("scheme=", i)) { if (inSelector) { intent.mData = Uri.parse(value + ":"); } else { scheme = value; } } // source bounds else if (uri.startsWith("sourceBounds=", i)) { intent.mSourceBounds = Rect.unflattenFromString(value); } // selector else if (semi == (i+3) && uri.startsWith("SEL", i)) { intent = new Intent(); inSelector = true; } // extra data parse else { String key = Uri.decode(uri.substring(i + 2, eq)); // create Bundle if it doesn't already exist if (intent.mExtras == null) intent.mExtras = new Bundle(); Bundle b = intent.mExtras; // add EXTRA,这里巧妙的对基本数据类型进行处理,(字母.var)值得学习借鉴 if (uri.startsWith("S.", i)) b.putString(key, value); else if (uri.startsWith("B.", i)) b.putBoolean(key, Boolean.parseBoolean(value)); else if (uri.startsWith("b.", i)) b.putByte(key, Byte.parseByte(value)); else if (uri.startsWith("c.", i)) b.putChar(key, value.charAt(0)); else if (uri.startsWith("d.", i)) b.putDouble(key, Double.parseDouble(value)); else if (uri.startsWith("f.", i)) b.putFloat(key, Float.parseFloat(value)); else if (uri.startsWith("i.", i)) b.putInt(key, Integer.parseInt(value)); else if (uri.startsWith("l.", i)) b.putLong(key, Long.parseLong(value)); else if (uri.startsWith("s.", i)) b.putShort(key, Short.parseShort(value)); else throw new URISyntaxException(uri, "unknown EXTRA type", i); } // move to the next item i = semi + 1; } if (inSelector) { // The Intent had a selector; fix it up. if (baseIntent.mPackage == null) { baseIntent.setSelector(intent); } intent = baseIntent; } ...... return intent;//解析生产内容,对应到生产的intent,返回处理 } catch (IndexOutOfBoundsException e) { throw new URISyntaxException(uri, "illegal Intent URI format", i); } }
经过上面简要分析,我们了解Uri intent生成过程,这种设计非常巧妙可以达到很好的解耦处理,相比于显示与隐士启动方式,这种方式更加灵活。
例如,我们可以由Server端动态下发,本地通过解析后在进行跳转页面,即可达到动态控制页面功能,其实不仅可以应用到页面跳转中,如果进一步扩展,可以应用到更多功能中,
比如我们可以扩展自定义规则,如header改为:http,https,代表页面跳转到webActivity,header改为:tipe时为toast提示,改为dialog时为弹框显示等。
我们还可以更为细致的控制,如可加入版本号指定对应的版本的app执行这规则,其余版本默认行为,适用于修复部分bug。由此可见我们可以通过修改parseUri方法即可以扩展到更多功能中,下面看一下我的修改,
static final String TAG="PageJump"; static final String SCHEME_INTENT = "page"; static final String SCHEME_ANDROIDAPP = "android-app:"; static final String SCHEME_HTTP = "http"; static final String SCHEME_HTTPS = "https"; static final String SCHEME_TIPS_DIALOG = "tips_dialog"; static final String SCHEME_TIPS_TOAST = "tips_toast"; //动态解析实现对页面行为控制 public static void jumpPageUri(Context context, String strUri) throws URISyntaxException{ //{(2,5][8,12)}android-app://com.example.app/<br />#Intent;action=com.example.MY_ACTION; if (TextUtils.isEmpty(strUri)) { throw new NullPointerException("parser uri content is empty"); } String data = strUri.trim(); //uri是否在版本内 final int curVer = Utils.getVerCode(context); if(isInRangeVersion(data,curVer)){ return; } //remove command version part if(data.startsWith("{")) { int verEnd = data.indexOf('}', 1); data = data.substring(verEnd+1); } String uriData = data; if(uriData.startsWith(SCHEME_ANDROIDAPP)){ Intent intent = Intent.parseUri(uriData, Intent.URI_INTENT_SCHEME); String appPackage = context.getPackageName(); ComponentName componentName = intent.getComponent(); //out intent if (componentName == null || !appPackage.contains(componentName.getPackageName())){ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } } else if (uriData.startsWith(SCHEME_INTENT)) { Intent sendIntent = UriProcessor.parseIntentUri(data, Intent.URI_INTENT_SCHEME); // Verify that the intent will resolve to an activity // if (null == sendIntent.resolveActivity(context.getPackageManager())) { // throw new URISyntaxException(data, "not found match page"); // } sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(sendIntent); } else if (uriData.startsWith(SCHEME_HTTP) || uriData.startsWith(SCHEME_HTTPS)) { // WebViewActivity.launch(context, uri); } else if(uriData.startsWith(SCHEME_TIPS_DIALOG)){ // DialogUtil.showNormal("test"); } else if(uriData.startsWith(SCHEME_TIPS_TOAST)){ // ToastUtils.showShortMessage(""); } }
规则串前面增加了应用版本范围,{(2,5][8,12)},这里我使用开闭区间的方式来指定及范围,这种方式更精简使用,版本解析处理
/** * current command version whether contain current app version * @param data * @param curVer * @return */ public static boolean isInRangeVersion(String data,final int curVer){ if(data.startsWith("{")){ int verEnd = data.indexOf('}', 1); if(verEnd>0) { String verStr = data.substring(0, verEnd+1); boolean in_range=true; int pos=1; try { while (pos >= 0 && !verStr.startsWith("}")) { in_range=true; char ch = verStr.charAt(pos); if (ch == '[' || ch == '(') { boolean[] border=new boolean[2]; int semi = verStr.indexOf(',', pos); int startVer = Integer.valueOf(verStr.substring(pos + 1, semi)); border[0]= (ch=='['); int toVer = 0, flagVer = 0; if ((flagVer = verStr.indexOf(']', semi)) >= 0 || (flagVer = verStr.indexOf(')', semi)) >= 0) { toVer = Integer.valueOf(verStr.substring(semi + 1, flagVer)); border[1]= (verStr.charAt(flagVer)==']'); } // judge current version code not inside range // jude min version code < <= if((border[0] && curVer<startVer) ||(!border[0] && curVer<=startVer)){ in_range=false; } // judge max version code > >= if((border[1] && curVer>toVer) ||(!border[1] && curVer>=toVer)){ in_range=false; } pos = flagVer + 1; if (pos + 1 >= verStr.length()) break; } } return in_range; }catch (NumberFormatException ex){ Log.e(TAG,"parse regular expression version error!"); } } return true; } return true; }
测试使用:
// String jumpUri1="{(2,5][8,12)}android-app://com.example.app/#Intent;action=com.example.MY_ACTION;i.some_int=100;S.some_str=hello;end"; String jumpUri2="{(0,3][6,12)}android-app://com.example.app/#Intent;action=com.example.MY_ACTION;i.some_int=100;end"; String jumpUri3="{(0,6]}android-app://com.example.app/#Intent;action=com.example.MY_ACTION;i.some_int=100;end"; String jumpUriPage="{(2,6]}android-app://com.example.myapp/#Intent;action=com.example.myapp.SecondActivity;package=com.example.myapp;category=android.intent.category.DEFAULT;S.some=systemFrom;end"; String jumpUriPage2="{[1,8]}page#Intent;action=com.example.myaction;package=com.example.myapp;category=android.intent.category.DEFAULT;S.some=innerFrom;end"; try { // PageJump.jumpPageUri(getApplicationContext(),jumpUri1); PageJump.jumpPageUri(getApplicationContext(),jumpUri2); PageJump.jumpPageUri(getApplicationContext(),jumpUri3); } catch (URISyntaxException e) { e.printStackTrace(); }
分析intent的代码设计后,真是觉得源码设计的十分巧妙,值得仔细认真琢磨。