Android 2.3.5 设置邮件中电话号码高亮显示
2012-04-20 11:24
309 查看
事情的起因是这样的,因为需要在Email的显示界面中为电话号码添加高亮显示,从而提高用户体验。我想用过google自带邮箱的朋友都知道,当我们的收到邮件的内容中含有网址,邮箱地址时,会在显示界面中以超链接的方式显示,此时当我们点击该网址或者邮箱地址时,便会弹出浏览器或者撰写邮件。目前的情况是,当我们的邮件内容中含有可识别的电话号码时,我们可以点击并跳转到拨号界面,但却不能高亮显示,因此我们的目标就是将电话号码修改为高亮显示,如下图1:
图1:修改后的效果
那么我们还是列出我们的目的吧,以免忘记(我就容易忘记:D)。
目标:将接收邮件内容中的电话号码高亮显示。
因为自己也是第一次接触邮件这块,因此可能走了很多弯路或者说的并不清除,请别见笑,如果后文中有错误还恳请各位朋友指正。毕竟,没有人一生下来就是老鸟!!!
好了废话不多说,开始吧!
1.找到问题的根源
需要修改的Email读取邮件的界面,当我看到该界面的时候第一反应,这货应该是一个TextView,用于撰写Email的界面应该是一个EditText。毕竟TextView用于显示这些内容足够了啊,进而联想到xml布局文件中的android:autoLink属性,只要为该TextView控件加上该属性则在TextView中的网址,邮件地址,电话号码等自动会变为超链接(不信的可以自己试试)。有了这个思路那么我们首先应该找到显示邮件内容的布局文件(这里提一下,1.如果像我等小白的话可能就会老老实实的去找packages/apps/Email/res/layout,然后再去看哪个名字像是显示内容的,比如:Mmessage_content.xml这种就很像,我一开始就是这么找的T_T。2.如果以前看过源码的朋友估计会通过查看该APP的应用程序入口,然后根据如果跳转到邮件显示Activity最后再搜索setContentView方法就可以知道其布局文件。3.其实其实,SDK/tools文件夹下真的有好多有用的东西——hierarchyviewer就是其中之一,我们只要运行该工具同时连接上我们的设备,也可以是模拟器,该工具会自动列出当前界面的布局状态,如图2所示)。
图2 hierarchyviewer显示
我用的是Ubuntu10.04 64bit 在windows下都差不多的,hierarchyviewer存放在SDK下的tools目录中,通过hierarchyviewer我可轻松的分析该界面的布局状态。鼠标左键选中以上布局文件时,右边界面中会有提示。因此在这里我们可以很轻松的将我们前面的假设推翻,显示界面根本就不是什么TextView而是WebView(小白就是小白啊T_T)。怎么会是WebView呢,稍稍做过web开发的人都知道的吧,我们要在界面中显示图片,超链接,邮件等等,这些东西如果要放到TextView中去做???很明显应该选择WebView嘛,同时我们从服务其获得的信息就是html字符串(后文会提到)。因此前面的问题就变成了在WebView中对电话号码进行高亮显示。
2.查看源代码
Linux之父Linus曾说过,要了解程序原理的最好方法就是——Read the fucking source code.好吧,通过查看程序入口,然后跳转到显示界面这些步骤省略,最终我们找到/packages/apps/Email/src/com/android/email/activity/MessageView.java,该类主要完成消息内容的显示。那么如何开始查看呢?因为我们主要关心的WebView的设置,因此我们可以直接搜索WebView来查看,结果全文中只有一个WebView:
因此我们可以继续搜索mMessageContentView这个对象,我们可以在onCreate方法中找到:
这里对该WebView对象进行了一些设置。
(1).setClickable(true)是设置WebView可点击;
(2).setLongClickable(false)是该WebView不支持长点击事件,这是因为会和ScrollView冲突,因此将其设置位false,可以在ScrollView中处理长点击事件;
(3).setVerticalScrollBarEnabled(false)接着设置垂直方向滚动禁止,也是和ScrollView冲突;
(4).setBlcokNetworkLoads(true)设置是否阻止网络加载,如果邮件中含有图片则会打开该属性;
(5).setSupportZoom(false)这是设置当前WebView不支持缩放;
(6).setWebViewClient使WebView接收到各种通知和请求;
接下来我们继续查找mMessageContentView可以发现mMessageContentView.loadDataWithBaseURL(...)方法,通过命名我们可以知道该方法用于加载URL同时,其中的参数很是奇怪,有text,mHtmlTextWebView等,这些东西是什么呢,果断加入Log.i("TAG","text:"+text);将这些信息通过log.i输出来,加入这些代码之后单独编译Email并将修改之后的Email.apk
push到/system/app路径下实践,根据打印出来的log我们可以初步将目光锁定到:
根据打印出来的log信息(这里就不贴log了,大家可以自己添加log打印出来看看),我们可以知道邮件内容有两种类型:bodyText和bodyHtml。前者不包括Html标签,那么这是哪两种情况呢?
(1).bodyText是对方通过Android邮箱给你发送邮件时,我们接收到的邮件信息是bodyText,不带Html标签;
(2).bodyHtml是通过网络邮箱发送时,我们接收到的信息是bodyHtml,带有Html标签;
根据打印出来的log信息我们可以看到:
而电话号码却没有任何提示,做过web开发的人肯定一看就明白了,不过我这里作为小菜的我还是要罗嗦几句,href在Html中表示超链接如果要了解详情的请自己去百度哈。很明显嘛,原始的bodyText数据不带这些标签的,经过处理(正则匹配)之后加上标签,然后通loadDataWithBaseURL(...)方法来完成加载显示的动作。那么回想以下我们的目标是什么?不就是为电话号码加入高亮显示么!因此我们只需要通过正则表达式匹配出邮件内容中的电话号码,同时给它加上类似于邮件和网址的标签就可以了吧!!!
3.深入分析并实现功能
有以上的分析,我们便可以开始动作实践了。但是问题又来了,要实现电话号码高亮的功能我们需要两个条件,第一,用于匹配电话号码的正则表达式,用于匹配邮件内容中的电话号码;第二,因为网址和邮件的高亮都有自己特殊的标志(邮件对应的是mailto),那么电话号码会不会也有对应的标志呢?在进一步分析前,我们先回过头想一想。虽然在邮件中我们的电话号码没有高亮,但是我们依然可以点击该号码跳转到拨号界面啊。也就是说该程序是识别到了电话号码的,只是没有高亮显示而已。继续分析,前面我们提到了,邮件显示网址和邮箱地址这些都是经过了正则匹配的(前面代码中),也就是匹配之后才知道是否为网址和邮箱地址。那么既然电话号码可以拨打肯定也是经过了正则匹配的。分析到这里,我们所需要的的两个条件之一已经基本解决,剩下的就是去找到已经定义好的电话号码匹配正则表达式。那么第二个问题呢?看似没有思路啊(0.0)!没关系,我们先来看看以下代码吧:
这里也获取不到什么有用的信息,但是这个方法是最终实现加载内容并显示的方法,因此它里面肯定有很多重要的东西,那么我们果断跳转跟踪它的实现吧。直接跳转到了frameworks/base/core/java/android/webkit/WebView.java中(小白解惑跳转方法:对准loadDataWithBaseURL方法名同时按住左Ctrl+鼠标左键)。代码如下:
在该方法中将传入的参数封装到了BaseUrlData对象arg中,然后通过sendMessage()方法将arg对象发送出去。
继续跟踪
哈哈终于看到熟悉的Handler了,既然是通过mHandler发送消息,那么肯定会有接收消息的handleMessage()方法啊,直接搜索mHandler的handleMessage()方法发现:
这里我只截取了我们需要的部分LOAD_DATA,分析该Case似乎也不能知道什么,但有两个关键点:第一,这里的scheme似乎可以用来匹配数据类型;第二,loadParams.mData似乎就是我们前面传入的data参数,该参数是加上了html标签的邮件内容;针对以上两点我们可以在这里打印log信息,然后单独编译framework,替换到设备中原framework。这样,再次运行的时候便会打印出log信息。这里我就分别打印出scheme和loadParams.mData两个字符串。经过实践以后验证了我的两个猜想,scheme中存放的是email://,就是我们前面传入的标志。loadParams.mData中存放的就是加入了html标签的邮件内容字符串。
到这里似乎没有眉目了啊。我们是想找到href中关于电话号码的关键词,这样可以使电话号码高亮显示,但跟踪到这里似乎并不能得到有用的信息。但我们可以知道,WebView的最终显示是通过将数据传递到JNI再到底层去实现的,有兴趣的朋友可以自己再去跟踪下。这里的nativeRegisterURLSchemeAsLocal位于external/webkit/WebKit/android/jni/WebViewCore.cpp中,对应的native方法是RegisterRULSchemeAsLocal()。继续跟踪可以跳转到/external/webkit/WebCore/page/SecurityOrigin.cpp中的SecurityOrigin::registerURLSchemeAsLocal()方法。后续没有再继续跟踪了,毕竟本人小菜一个,后面再进一步学习吧。
3.峰回路转柳暗花明
在前面饶了一大圈,头也晕了,我们还是整理整理思路吧。1.我们想找到设置电话号码高亮的标签(类似邮件的href=mailto)。2.找到用于匹配电话号码的正则表达式。经过了前面的查找,我们似乎还是无法找到电话号码的标签位。那我们换一下,这次先找匹配电话号码的正则表达式。首先,我们在代码:
中发现有Matcher m = Patterns.WEB_URL.matcher(text);这句代码就是在对text作正则匹配,从字面上意思来看就是匹配网址的。我们直接跳转到Patterns中可以看到,该类中存放着多种用于正则匹配的Patterns对象。一不小心,:-)发现了该Patterns对象:PHONE
这不正是我们苦苦寻找的用于匹配电话号码的正则表达式么?!那么我们接下来就只需要获取电话号码href后的标志就可以了。现在我们可以这样来思考,因为电话号码以及网址和邮箱地址都是可以点击的,因此我们去寻找该点击事件的触发点。看看该触发事件是根据什么来判断的(猜想是通过独特的Flag完成:D)。
还记得onCreate()方法中的WebView初始化么,setWebViewClient方法就是让WebView接收各种请求信息,当然要包括了超链接这种啊。因此,找到其中CustomWebViewClient类实现,代码如下:
该类中override了shouldOverrideUrlLoading方法,在该方法中我们可以清楚的看到url来匹配mailto:标志,因此我们可以大胆的猜想,根据这里匹配来确定到底点击了哪种链接,从而通过intent跳转到不同的Activity。那么我为何不将url打印出来呢?哈哈,赶快加入log,编译Email.apk,安装调试。果然不出我们的所料,打印出来的log信息正是:tel:13800174444。看到木有,看到木有,电话号码的标签就是tel啊。
4.最终实现
经过了这么复杂的过程,我们所需要的东西都已经拿到了,那么赶快动手修改代码吧。前面我们就已经分析过了,要加入高亮显示,只需要对接收到的text文件进行修改就可以了。修改后的代码如下:
这里我加入了自己的修改方法highLight(int TYPE,String text),这里的TYPE表示需要高亮的对象,包括了PHONENUM,EMAILADDRESS,WEBURL,ALL四种,其中ALL表示包含前面所有;text表示需要高亮的字符串;因为bodyText和bodyHtml获取到的数据不同,前面有提到,bodyText是纯文本String;bodyHtml是具有html标签的文本,同时BodyHtml是网络邮箱返回的数据,其中有的已经是对EmailAddress和web
url处理过的,但有的还是没有,比如像126,163等邮箱,他们返回的bodyHtml中如果含有url或者email addrss,它也不会将其置为高亮,因此这里通过ALL来全部处理。最后贴出自己的highLight方法:
针对以上代码做下简单的分析,其实代码的功能就是将找到需要替换的字符串然后在替换。比如,原:"你好,这是简单123的测试",替换:"你好,这是简单<a href="tel:123>"123</a>的测试"。
5.总结
经过了各种纠结,总算完成了需要的功能了。在此也小小的总结以下,对于SDK里面提供的工具自己还是应该多熟悉以下。其次应该使用StarUML将代码执行流程图画出来,这样一来直观而来也便于交流。最后,对于这种总结自己应该多做一些,一方面可以督促自己尽量去搞懂,另一方面也可以检查自己的知识漏洞。
图1:修改后的效果
那么我们还是列出我们的目的吧,以免忘记(我就容易忘记:D)。
目标:将接收邮件内容中的电话号码高亮显示。
因为自己也是第一次接触邮件这块,因此可能走了很多弯路或者说的并不清除,请别见笑,如果后文中有错误还恳请各位朋友指正。毕竟,没有人一生下来就是老鸟!!!
好了废话不多说,开始吧!
1.找到问题的根源
需要修改的Email读取邮件的界面,当我看到该界面的时候第一反应,这货应该是一个TextView,用于撰写Email的界面应该是一个EditText。毕竟TextView用于显示这些内容足够了啊,进而联想到xml布局文件中的android:autoLink属性,只要为该TextView控件加上该属性则在TextView中的网址,邮件地址,电话号码等自动会变为超链接(不信的可以自己试试)。有了这个思路那么我们首先应该找到显示邮件内容的布局文件(这里提一下,1.如果像我等小白的话可能就会老老实实的去找packages/apps/Email/res/layout,然后再去看哪个名字像是显示内容的,比如:Mmessage_content.xml这种就很像,我一开始就是这么找的T_T。2.如果以前看过源码的朋友估计会通过查看该APP的应用程序入口,然后根据如果跳转到邮件显示Activity最后再搜索setContentView方法就可以知道其布局文件。3.其实其实,SDK/tools文件夹下真的有好多有用的东西——hierarchyviewer就是其中之一,我们只要运行该工具同时连接上我们的设备,也可以是模拟器,该工具会自动列出当前界面的布局状态,如图2所示)。
图2 hierarchyviewer显示
我用的是Ubuntu10.04 64bit 在windows下都差不多的,hierarchyviewer存放在SDK下的tools目录中,通过hierarchyviewer我可轻松的分析该界面的布局状态。鼠标左键选中以上布局文件时,右边界面中会有提示。因此在这里我们可以很轻松的将我们前面的假设推翻,显示界面根本就不是什么TextView而是WebView(小白就是小白啊T_T)。怎么会是WebView呢,稍稍做过web开发的人都知道的吧,我们要在界面中显示图片,超链接,邮件等等,这些东西如果要放到TextView中去做???很明显应该选择WebView嘛,同时我们从服务其获得的信息就是html字符串(后文会提到)。因此前面的问题就变成了在WebView中对电话号码进行高亮显示。
2.查看源代码
Linux之父Linus曾说过,要了解程序原理的最好方法就是——Read the fucking source code.好吧,通过查看程序入口,然后跳转到显示界面这些步骤省略,最终我们找到/packages/apps/Email/src/com/android/email/activity/MessageView.java,该类主要完成消息内容的显示。那么如何开始查看呢?因为我们主要关心的WebView的设置,因此我们可以直接搜索WebView来查看,结果全文中只有一个WebView:
private WebView mMessageContentView;
因此我们可以继续搜索mMessageContentView这个对象,我们可以在onCreate方法中找到:
mMessageContentView.setClickable(true); mMessageContentView.setLongClickable(false); // Conflicts with ScrollView, unfortunately mMessageContentView.setVerticalScrollBarEnabled(false); mMessageContentView.getSettings().setBlockNetworkLoads(true); mMessageContentView.getSettings().setSupportZoom(false); mMessageContentView.setWebViewClient(new CustomWebViewClient());
这里对该WebView对象进行了一些设置。
(1).setClickable(true)是设置WebView可点击;
(2).setLongClickable(false)是该WebView不支持长点击事件,这是因为会和ScrollView冲突,因此将其设置位false,可以在ScrollView中处理长点击事件;
(3).setVerticalScrollBarEnabled(false)接着设置垂直方向滚动禁止,也是和ScrollView冲突;
(4).setBlcokNetworkLoads(true)设置是否阻止网络加载,如果邮件中含有图片则会打开该属性;
(5).setSupportZoom(false)这是设置当前WebView不支持缩放;
(6).setWebViewClient使WebView接收到各种通知和请求;
接下来我们继续查找mMessageContentView可以发现mMessageContentView.loadDataWithBaseURL(...)方法,通过命名我们可以知道该方法用于加载URL同时,其中的参数很是奇怪,有text,mHtmlTextWebView等,这些东西是什么呢,果断加入Log.i("TAG","text:"+text);将这些信息通过log.i输出来,加入这些代码之后单独编译Email并将修改之后的Email.apk
push到/system/app路径下实践,根据打印出来的log我们可以初步将目光锁定到:
/** * Reload the body from the provider cursor. This must only be called from the UI thread. * * @param bodyText text part * @param bodyHtml html part * * TODO deal with html vs text and many other issues */ private void reloadUiFromBody(String bodyText, String bodyHtml) { String text = null; mHtmlTextRaw = null; boolean hasImages = false; if (bodyHtml == null) { text = bodyText; /* * Convert the plain text to HTML */ StringBuffer sb = new StringBuffer("<html><body>"); if (text != null) { // Escape any inadvertent HTML in the text message text = EmailHtmlUtil.escapeCharacterToDisplay(text); // Find any embedded URL's and linkify Matcher m = Patterns.WEB_URL.matcher(text); while (m.find()) { int start = m.start(); /* * WEB_URL_PATTERN may match domain part of email address. To detect * this false match, the character just before the matched string * should not be '@'. */ if (start == 0 || text.charAt(start - 1) != '@') { String url = m.group(); Matcher proto = WEB_URL_PROTOCOL.matcher(url); String link; if (proto.find()) { // This is work around to force URL protocol part be lower case, // because WebView could follow only lower case protocol link. link = proto.group().toLowerCase() + url.substring(proto.end()); } else { // Patterns.WEB_URL matches URL without protocol part, // so added default protocol to link. link = "http://" + url; } String href = String.format("<a href=\"%s\">%s</a>", link, url); m.appendReplacement(sb, href); } else { m.appendReplacement(sb, "$0"); } } m.appendTail(sb); } sb.append("</body></html>"); text = sb.toString(); } else { text = bodyHtml; mHtmlTextRaw = bodyHtml; hasImages = IMG_TAG_START_REGEX.matcher(text).find(); } mShowPicturesSection.setVisibility(hasImages ? View.VISIBLE : View.GONE); if (mMessageContentView != null) { mMessageContentView.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null); } // Ask for attachments after body mLoadAttachmentsTask = new LoadAttachmentsTask(); mLoadAttachmentsTask.execute(mMessage.mId); }
根据打印出来的log信息(这里就不贴log了,大家可以自己添加log打印出来看看),我们可以知道邮件内容有两种类型:bodyText和bodyHtml。前者不包括Html标签,那么这是哪两种情况呢?
(1).bodyText是对方通过Android邮箱给你发送邮件时,我们接收到的邮件信息是bodyText,不带Html标签;
(2).bodyHtml是通过网络邮箱发送时,我们接收到的信息是bodyHtml,带有Html标签;
根据打印出来的log信息我们可以看到:
<a href="http://www.baidu.com">http://www.baidu.com</a> <a href="mailto:GRDTencent@qq.com">GRDTencent@qq.com</a>
而电话号码却没有任何提示,做过web开发的人肯定一看就明白了,不过我这里作为小菜的我还是要罗嗦几句,href在Html中表示超链接如果要了解详情的请自己去百度哈。很明显嘛,原始的bodyText数据不带这些标签的,经过处理(正则匹配)之后加上标签,然后通loadDataWithBaseURL(...)方法来完成加载显示的动作。那么回想以下我们的目标是什么?不就是为电话号码加入高亮显示么!因此我们只需要通过正则表达式匹配出邮件内容中的电话号码,同时给它加上类似于邮件和网址的标签就可以了吧!!!
3.深入分析并实现功能
有以上的分析,我们便可以开始动作实践了。但是问题又来了,要实现电话号码高亮的功能我们需要两个条件,第一,用于匹配电话号码的正则表达式,用于匹配邮件内容中的电话号码;第二,因为网址和邮件的高亮都有自己特殊的标志(邮件对应的是mailto),那么电话号码会不会也有对应的标志呢?在进一步分析前,我们先回过头想一想。虽然在邮件中我们的电话号码没有高亮,但是我们依然可以点击该号码跳转到拨号界面啊。也就是说该程序是识别到了电话号码的,只是没有高亮显示而已。继续分析,前面我们提到了,邮件显示网址和邮箱地址这些都是经过了正则匹配的(前面代码中),也就是匹配之后才知道是否为网址和邮箱地址。那么既然电话号码可以拨打肯定也是经过了正则匹配的。分析到这里,我们所需要的的两个条件之一已经基本解决,剩下的就是去找到已经定义好的电话号码匹配正则表达式。那么第二个问题呢?看似没有思路啊(0.0)!没关系,我们先来看看以下代码吧:
mMessageContentView.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null);
这里也获取不到什么有用的信息,但是这个方法是最终实现加载内容并显示的方法,因此它里面肯定有很多重要的东西,那么我们果断跳转跟踪它的实现吧。直接跳转到了frameworks/base/core/java/android/webkit/WebView.java中(小白解惑跳转方法:对准loadDataWithBaseURL方法名同时按住左Ctrl+鼠标左键)。代码如下:
public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) { loadData(data, mimeType, encoding); return; } switchOutDrawHistory(); WebViewCore.BaseUrlData arg = new WebViewCore.BaseUrlData(); arg.mBaseUrl = baseUrl; arg.mData = data; arg.mMimeType = mimeType; arg.mEncoding = encoding; arg.mHistoryUrl = historyUrl; mWebViewCore.sendMessage(EventHub.LOAD_DATA, arg); clearHelpers(); }
在该方法中将传入的参数封装到了BaseUrlData对象arg中,然后通过sendMessage()方法将arg对象发送出去。
void sendMessage(int what, Object obj) { mEventHub.sendMessage(Message.obtain(null, what, obj)); }
继续跟踪
private synchronized void sendMessage(Message msg) { if (mBlockMessages) { return; } if (mMessages != null) { mMessages.add(msg); } else { mHandler.sendMessage(msg); } }
哈哈终于看到熟悉的Handler了,既然是通过mHandler发送消息,那么肯定会有接收消息的handleMessage()方法啊,直接搜索mHandler的handleMessage()方法发现:
mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (DebugFlags.WEB_VIEW_CORE) { Log.v(LOGTAG, (msg.what < REQUEST_LABEL || msg.what > VALID_NODE_BOUNDS ? Integer.toString(msg.what) : HandlerDebugString[msg.what - REQUEST_LABEL]) + " arg1=" + msg.arg1 + " arg2=" + msg.arg2 + " obj=" + msg.obj); } switch (msg.what) { case WEBKIT_DRAW: webkitDraw(); break; ... case LOAD_DATA: BaseUrlData loadParams = (BaseUrlData) msg.obj; String baseUrl = loadParams.mBaseUrl; if (baseUrl != null) { int i = baseUrl.indexOf(':'); if (i > 0) { /* * In 1.0, {@link * WebView#loadDataWithBaseURL} can access * local asset files as long as the data is * valid. In the new WebKit, the restriction * is tightened. To be compatible with 1.0, * we automatically add the scheme of the * baseUrl for local access as long as it is * not http(s)/ftp(s)/about/javascript */ String scheme = baseUrl.substring(0, i); if (!scheme.startsWith("http") && !scheme.startsWith("ftp") && !scheme.startsWith("about") && !scheme.startsWith("javascript")) { nativeRegisterURLSchemeAsLocal(scheme); } } } mBrowserFrame.loadData(baseUrl, loadParams.mData, loadParams.mMimeType, loadParams.mEncoding, loadParams.mHistoryUrl); break; ... case START_DNS_PREFETCH: mBrowserFrame.startDnsPrefetch(); break; } } };
这里我只截取了我们需要的部分LOAD_DATA,分析该Case似乎也不能知道什么,但有两个关键点:第一,这里的scheme似乎可以用来匹配数据类型;第二,loadParams.mData似乎就是我们前面传入的data参数,该参数是加上了html标签的邮件内容;针对以上两点我们可以在这里打印log信息,然后单独编译framework,替换到设备中原framework。这样,再次运行的时候便会打印出log信息。这里我就分别打印出scheme和loadParams.mData两个字符串。经过实践以后验证了我的两个猜想,scheme中存放的是email://,就是我们前面传入的标志。loadParams.mData中存放的就是加入了html标签的邮件内容字符串。
到这里似乎没有眉目了啊。我们是想找到href中关于电话号码的关键词,这样可以使电话号码高亮显示,但跟踪到这里似乎并不能得到有用的信息。但我们可以知道,WebView的最终显示是通过将数据传递到JNI再到底层去实现的,有兴趣的朋友可以自己再去跟踪下。这里的nativeRegisterURLSchemeAsLocal位于external/webkit/WebKit/android/jni/WebViewCore.cpp中,对应的native方法是RegisterRULSchemeAsLocal()。继续跟踪可以跳转到/external/webkit/WebCore/page/SecurityOrigin.cpp中的SecurityOrigin::registerURLSchemeAsLocal()方法。后续没有再继续跟踪了,毕竟本人小菜一个,后面再进一步学习吧。
3.峰回路转柳暗花明
在前面饶了一大圈,头也晕了,我们还是整理整理思路吧。1.我们想找到设置电话号码高亮的标签(类似邮件的href=mailto)。2.找到用于匹配电话号码的正则表达式。经过了前面的查找,我们似乎还是无法找到电话号码的标签位。那我们换一下,这次先找匹配电话号码的正则表达式。首先,我们在代码:
private void reloadUiFromBody(String bodyText, String bodyHtml) { String text = null; mHtmlTextRaw = null; boolean hasImages = false; if (bodyHtml == null) { text = bodyText; /* * Convert the plain text to HTML */ StringBuffer sb = new StringBuffer("<html><body>"); if (text != null) { // Escape any inadvertent HTML in the text message text = EmailHtmlUtil.escapeCharacterToDisplay(text); // Find any embedded URL's and linkify Matcher m = Patterns.WEB_URL.matcher(text); while (m.find()) { int start = m.start(); /* * WEB_URL_PATTERN may match domain part of email address. To detect * this false match, the character just before the matched string * should not be '@'. */ if (start == 0 || text.charAt(start - 1) != '@') { String url = m.group(); Matcher proto = WEB_URL_PROTOCOL.matcher(url); String link; if (proto.find()) { // This is work around to force URL protocol part be lower case, // because WebView could follow only lower case protocol link. link = proto.group().toLowerCase() + url.substring(proto.end()); } else { // Patterns.WEB_URL matches URL without protocol part, // so added default protocol to link. link = "http://" + url; } String href = String.format("<a href=\"%s\">%s</a>", link, url); m.appendReplacement(sb, href); } else { m.appendReplacement(sb, "$0"); } } m.appendTail(sb); } sb.append("</body></html>"); text = sb.toString(); } else { text = bodyHtml; mHtmlTextRaw = bodyHtml; hasImages = IMG_TAG_START_REGEX.matcher(text).find(); } mShowPicturesSection.setVisibility(hasImages ? View.VISIBLE : View.GONE); if (mMessageContentView != null) { mMessageContentView.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null); } // Ask for attachments after body mLoadAttachmentsTask = new LoadAttachmentsTask(); mLoadAttachmentsTask.execute(mMessage.mId); }
中发现有Matcher m = Patterns.WEB_URL.matcher(text);这句代码就是在对text作正则匹配,从字面上意思来看就是匹配网址的。我们直接跳转到Patterns中可以看到,该类中存放着多种用于正则匹配的Patterns对象。一不小心,:-)发现了该Patterns对象:PHONE
public static final Pattern PHONE = Pattern.compile( // sdd = space, dot, or dash "(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>* + "(\\([0-9]+\\)[\\- \\.]*)?" // (<digits>)<sdd>* + "([0-9][0-9\\- \\.][0-9\\- \\.]+[0-9])"); // <digit><digit|sdd>+<digit>
这不正是我们苦苦寻找的用于匹配电话号码的正则表达式么?!那么我们接下来就只需要获取电话号码href后的标志就可以了。现在我们可以这样来思考,因为电话号码以及网址和邮箱地址都是可以点击的,因此我们去寻找该点击事件的触发点。看看该触发事件是根据什么来判断的(猜想是通过独特的Flag完成:D)。
mMessageContentView.setWebViewClient(new CustomWebViewClient());
还记得onCreate()方法中的WebView初始化么,setWebViewClient方法就是让WebView接收各种请求信息,当然要包括了超链接这种啊。因此,找到其中CustomWebViewClient类实现,代码如下:
private class CustomWebViewClient extends WebViewClient { /** * This is intended to mirror the operation of the original * (see android.webkit.CallbackProxy) with one addition of intent flags * "FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET". This improves behavior when sublaunching * other apps via embedded URI's. * * We also use this hook to catch "mailto:" links and handle them locally. */ @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { // hijack mailto: uri's and handle locally if (url != null && url.toLowerCase().startsWith("mailto:")) { return MessageCompose.actionCompose(MessageView.this, url, mAccountId); } // Handle most uri's via intent launch boolean result = false; Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); intent.addCategory(Intent.CATEGORY_BROWSABLE); intent.putExtra(Browser.EXTRA_APPLICATION_ID, getPackageName()); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); try { startActivity(intent); result = true; } catch (ActivityNotFoundException ex) { // If no application can handle the URL, assume that the // caller can handle it. } return result; } }
该类中override了shouldOverrideUrlLoading方法,在该方法中我们可以清楚的看到url来匹配mailto:标志,因此我们可以大胆的猜想,根据这里匹配来确定到底点击了哪种链接,从而通过intent跳转到不同的Activity。那么我为何不将url打印出来呢?哈哈,赶快加入log,编译Email.apk,安装调试。果然不出我们的所料,打印出来的log信息正是:tel:13800174444。看到木有,看到木有,电话号码的标签就是tel啊。
4.最终实现
private void reloadUiFromBody(String bodyText, String bodyHtml) { String text = null; mHtmlTextRaw = null; boolean hasImages = false; if (bodyHtml == null) { text = bodyText; /* * Convert the plain text to HTML */ StringBuffer sb = new StringBuffer("<html><body>"); if (text != null) { // Escape any inadvertent HTML in the text message text = EmailHtmlUtil.escapeCharacterToDisplay(text); // Find any embedded URL's and linkify Matcher m = Patterns.WEB_URL.matcher(text); while (m.find()) { int start = m.start(); /* * WEB_URL_PATTERN may match domain part of email address. To detect * this false match, the character just before the matched string * should not be '@'. */ if (start == 0 || text.charAt(start - 1) != '@') { String url = m.group(); Matcher proto = WEB_URL_PROTOCOL.matcher(url); String link; if (proto.find()) { // This is work around to force URL protocol part be lower case, // because WebView could follow only lower case protocol link. link = proto.group().toLowerCase() + url.substring(proto.end()); } else { // Patterns.WEB_URL matches URL without protocol part, // so added default protocol to link. link = "http://" + url; } String href = String.format("<a href=\"%s\">%s</a>", link, url); m.appendReplacement(sb, href); } else { m.appendReplacement(sb, "$0"); } } m.appendTail(sb); } sb.append("</body></html>"); text = sb.toString(); text = highLight(ALL, text); } else { text = bodyHtml; text = highLight(PHONENUM, text); mHtmlTextRaw = bodyHtml; hasImages = IMG_TAG_START_REGEX.matcher(text).find(); } mShowPicturesSection.setVisibility(hasImages ? View.VISIBLE : View.GONE); if (mMessageContentView != null) { mMessageContentView.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null); } // Ask for attachments after body mLoadAttachmentsTask = new LoadAttachmentsTask(); mLoadAttachmentsTask.execute(mMessage.mId); }
经过了这么复杂的过程,我们所需要的东西都已经拿到了,那么赶快动手修改代码吧。前面我们就已经分析过了,要加入高亮显示,只需要对接收到的text文件进行修改就可以了。修改后的代码如下:
private void reloadUiFromBody(String bodyText, String bodyHtml) { String text = null; mHtmlTextRaw = null; boolean hasImages = false; if (bodyHtml == null) { text = bodyText; /* * Convert the plain text to HTML */ StringBuffer sb = new StringBuffer(""); if (text != null) { // Escape any inadvertent HTML in the text message text = EmailHtmlUtil.escapeCharacterToDisplay(text); // Find any embedded URL's and linkify Matcher m = Patterns.WEB_URL.matcher(text); while (m.find()) { int start = m.start(); /* * WEB_URL_PATTERN may match domain part of email address. To detect * this false match, the character just before the matched string * should not be '@'. */ if (start == 0 || text.charAt(start - 1) != '@') { String url = m.group(); Matcher proto = WEB_URL_PROTOCOL.matcher(url); String link; if (proto.find()) { // This is work around to force URL protocol part be lower case, // because WebView could follow only lower case protocol link. link = proto.group().toLowerCase() + url.substring(proto.end()); } else { // Patterns.WEB_URL matches URL without protocol part, // so added default protocol to link. link = "http://" + url; } String href = String.format("%s", link, url); m.appendReplacement(sb, href); } else { m.appendReplacement(sb, "$0"); } } m.appendTail(sb); } sb.append(""); text = sb.toString(); text = highLight(ALL,text);//全部高亮,highLight的参数有三种PHONENUM,EMAILADDRESS,WEBURL } else { text = bodyHtml; text = highLight(ALL,text);//为什么这里会是ALL?!后文会有讲解 mHtmlTextRaw = text;//这里也要注意修改哦 不然后面点击显示图片后文中的电话号码高亮会消失的哦:-) hasImages = IMG_TAG_START_REGEX.matcher(text).find(); } mShowPicturesSection.setVisibility(hasImages ? View.VISIBLE : View.GONE); if (mMessageContentView != null) { mMessageContentView.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null); } // Ask for attachments after body mLoadAttachmentsTask = new LoadAttachmentsTask(); mLoadAttachmentsTask.execute(mMessage.mId); }
这里我加入了自己的修改方法highLight(int TYPE,String text),这里的TYPE表示需要高亮的对象,包括了PHONENUM,EMAILADDRESS,WEBURL,ALL四种,其中ALL表示包含前面所有;text表示需要高亮的字符串;因为bodyText和bodyHtml获取到的数据不同,前面有提到,bodyText是纯文本String;bodyHtml是具有html标签的文本,同时BodyHtml是网络邮箱返回的数据,其中有的已经是对EmailAddress和web
url处理过的,但有的还是没有,比如像126,163等邮箱,他们返回的bodyHtml中如果含有url或者email addrss,它也不会将其置为高亮,因此这里通过ALL来全部处理。最后贴出自己的highLight方法:
/** * high light the telephone number and web url and email address. * @author http://blog.csdn.net/yihongyuelan * @param TYPE contains PHONENUM,EMAILADDRESS,WEBURL,ALL * @return String which has been replaced */ public String highLight(int TYPE,String text){ replacedString = text;//这里的replacedString是全局变量 String replaceElement = "";//表示需要处理的元素电话号码用"tel:"表示,同样,邮件是"mailto:" web url则是""(空) switch (TYPE) { case PHONENUM://只对电话号码进行高亮处理 pattern = Patterns.PHONE ;//用于正则匹配phone的patterns matcher = pattern.matcher(replacedString); replaceElement = PHONE_HIGHLIGHT_ELEMENT; while(FLAG){ replacedString = replaceKeyString(matcher,replaceElement);//主要的替换方法 matcher = getMatcher(replacedString); } FLAG = true; break; case EMAILADDRESS://只对邮件地址号码进行高亮处理 pattern = Patterns.EMAIL_ADDRESS ; matcher = pattern.matcher(replacedString); replaceElement = EMAILADDRESS_HIGHLIGHT_ELEMENT ; while(FLAG){ replacedString = replaceKeyString(matcher,replaceElement); matcher = getMatcher(replacedString); } FLAG = true; break; case WEBURL://只对web url进行高亮处理 pattern = Patterns.WEB_URL; matcher = pattern.matcher(replacedString); replaceElement = WEBURL_HIGHLIGHT_ELEMENT; while(FLAG){ replacedString = replaceKeyString(matcher,replaceElement); matcher = getMatcher(replacedString); } FLAG = true; break; case ALL://只对以上三者进行高亮处理 pattern = Patterns.PHONE; replaceElement = PHONE_HIGHLIGHT_ELEMENT; matcher = pattern.matcher(replacedString); while(FLAG){ replacedString = replaceKeyString(matcher,replaceElement); matcher = getMatcher(replacedString); } FLAG = true; pattern = Patterns.EMAIL_ADDRESS; replaceElement = EMAILADDRESS_HIGHLIGHT_ELEMENT; matcher = pattern.matcher(replacedString); while(FLAG){ replacedString = replaceKeyString(matcher,replaceElement); matcher = getMatcher(replacedString); } FLAG = true; pattern = Patterns.WEB_URL; replaceElement = WEBURL_HIGHLIGHT_ELEMENT; matcher = pattern.matcher(replacedString); while(FLAG){ replacedString = replaceKeyString(matcher,replaceElement); matcher = getMatcher(replacedString); } FLAG = true; default: break; } return replacedString; } /** * replace the key String. * @author http://blog.csdn.net/yihongyuelan * @param matcher Matcher * @param mReplaceElement replace element "" or "tel:" or "mailto:" * @return replacedString text has been replaced */ public String replaceKeyString(Matcher matcher, String mReplaceElement) { String replaceElement = mReplaceElement; String keyString = ""; String result = replacedString; while (matcher.find()) { keyString = matcher.group(); if (WEBURL_HIGHLIGHT_ELEMENT.equals(replaceElement)) {//在匹配web url时处理 int endUrl = matcher.start(); int startUrl = endUrl - 7; if (startUrl > 0) { String hrefStr = result.substring(startUrl, endUrl); if (hrefStr.contains("href")) { continue;//如果包含href说明已经处理过了,不再处理 } } int endSrc = matcher.start(); int startSrc = endSrc - 6; if (startSrc > 0) { String srcStr = result.substring(startSrc, endSrc); if (srcStr.contains("src")) { continue;//如果包含src说明是资源引用,不属于需要高亮的web url } } int endXMLNS = matcher.start(); int startXMLNS = endXMLNS - 14; if (startXMLNS > 0) { String xmlnsStr = result.substring(startXMLNS, endXMLNS); if (xmlnsStr.contains("xmlns")) { continue;//命名空间需要高亮 } } int endStr = matcher.start() - 2; int startStr = endStr - keyString.length(); if (startStr > 0) { String webStr = result.substring( endStr - keyString.length(), endStr); if (keyString.contains(webStr)) { continue;//已经处理过的不再需要处理 } } if(matcher.start()-1 >=0 && result.charAt(matcher.start()-1) == '@'){ continue;//不出里邮件格式如GRDTencent@qq.com } if(keyString.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){ continue;不出里数字格式 ru 10.128.10.10 } String url = keyString; String link; int start = matcher.start(); int end = start + 4; String flagStr = result.substring(start, end); if(!flagStr.contains("http") && !flagStr.contains("Http") && !flagStr.contains("HTTP") && !flagStr.contains("ftp") && !flagStr.contains("Ftp") && !flagStr.contains("FTP") && !flagStr.contains("rtsp") && !flagStr.contains("Rtsp") && !flagStr.contains("RTSP")){ link = "http://" + url; String href = String.format("%s", link, url); result = result.substring(0, matcher.start())+href+result.substring(matcher.end()); return result;//如果不包含http等的url 如www.baidu.com } } if (PHONE_HIGHLIGHT_ELEMENT.equals(replaceElement)) {//在匹配电话号码时处理 if (4 == keyString.length()) {//低于4个数字的不处理 continue; } if (matcher.start() - 1 >= 0) {//数字前面含有@ - 等特殊字符的不处理 char keyChar = result.charAt(matcher.start() - 1); if (keyChar == 45 || keyChar == 47 || keyChar == 61 || keyChar == 63 || (35 <= keyChar && keyChar <= 38)) { continue; } } int end = matcher.start(); int start = end - 8; if (start < 0) { start = end - 7; } if(start>=0){//不处理数字前面以http之类开头的 如http://10.128.12.211 String urlStr = result.substring(start, end); if (urlStr.contains("http") || urlStr.contains("Http") || urlStr.contains("HTTP") || urlStr.contains("ftp") || urlStr.contains("Ftp") || urlStr.contains("FTP") || urlStr.contains("rtsp") || urlStr.contains("Rtsp") || urlStr.contains("RTSP")) { continue; } } if (matcher.end() != result.length()) {//数字后面有字母包括或者@的不处理 char keyChar = result.charAt(matcher.end()); if ((64 <= keyChar && keyChar <= 122) || keyChar == 95) { continue; } } if (keyString.matches("\\d{1,4}\\-\\d{1,2}\\-\\d{1,2}(\\s\\d{1,2})?")) {//日期不处理 continue; } if(keyString.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){//如 10.120.121.22不处理 continue; } int endPhone = matcher.start(); int startPhone = endPhone - 4; String emailStr = result.substring(startPhone, endPhone); if (emailStr.contains("tel")) {//包含tel标志的 不处理 continue; } int endStr = matcher.start() - 2; int startStr = endStr - keyString.length(); if (startStr > 0) { String webStr = result.substring(endStr - keyString.length(), endStr); if (keyString.contains(webStr)) { continue;//已经处理过的不再处理 } } } if (EMAILADDRESS_HIGHLIGHT_ELEMENT.equals(replaceElement)) {//匹配邮件地址时 int endEmail = matcher.start(); int startEmail = endEmail - 7; String emailStr = result.substring(startEmail, endEmail); if (emailStr.contains("mailto")) {//已经有mailto标志的 不再匹配 continue; } int endStr = matcher.start() - 2; int startStr = endStr - keyString.length(); if (startStr > 0) { String webStr = result.substring(endStr - keyString.length(), endStr); if (keyString.contains(webStr)) { continue;//已经处理国的不再处理 } } } String replacement = "" + keyString + "";//开始替换 result = result.substring(0, matcher.start())+replacement+result.substring(matcher.end()); return result; } FLAG = false; return result; } /** * construct the matcher. * @author http://blog.csdn.net/yihongyuelan * @param String to construct matcher * @return matcher */ public Matcher getMatcher(String str) { Matcher matcher = pattern.matcher(str); return matcher; }
针对以上代码做下简单的分析,其实代码的功能就是将找到需要替换的字符串然后在替换。比如,原:"你好,这是简单123的测试",替换:"你好,这是简单<a href="tel:123>"123</a>的测试"。
5.总结
经过了各种纠结,总算完成了需要的功能了。在此也小小的总结以下,对于SDK里面提供的工具自己还是应该多熟悉以下。其次应该使用StarUML将代码执行流程图画出来,这样一来直观而来也便于交流。最后,对于这种总结自己应该多做一些,一方面可以督促自己尽量去搞懂,另一方面也可以检查自己的知识漏洞。
相关文章推荐
- 最新历史版本 :android--设置TextView部分文字的颜色和背景(高亮显示)
- android--设置TextView部分文字的颜色和背景(高亮显示)
- android--设置TextView部分文字的颜色和背景(高亮显示)
- android--设置TextView部分文字的颜色和背景(高亮显示)
- android--设置TextView部分文字的颜色和背景(高亮显示)
- android--设置TextView部分文字的颜色和背景(高亮显示)
- android--设置TextView部分文字的颜色和背景(高亮显示)
- android--设置TextView部分文字的颜色和背景(高亮显示)
- 最新历史版本 :android--设置TextView部分文字的颜色和背景(高亮显示)
- Android studio设置之相同变量名高亮显示
- 最新历史版本 :android--设置TextView部分文字的颜色和背景(高亮显示)
- android--设置TextView部分文字的颜色和背景(高亮显示)
- android listview选中某一行,成选中状态颜色高亮显示
- 设置vim语法高亮显示和自动缩进
- Android项目:手机安全卫士(10)—— 电话号码归属地显示
- android 自定义Dialog背景透明及显示位置设置的方法
- Android之设置横屏、竖屏和全屏显示
- 改变高亮显示的变量的背景色(默认暗灰色)在这里设置
- Android实战处理带+号的电话号码在Arabic语言中的正确显示
- MyEclipse设置JAVA选中高亮显示