分享是一个app产生用户数据的重要来源,也是app宣传拉新的重要途径,所以对于稍微成熟一点的app分享是必不可少的模块。相信稍微接触过分享的人都清楚,分享到外部app很简单,只要接入微信、微博、QQ等提供好的sdk并按照规定好的参数正确调用api就好了。这也正是为何这些app能用美观的方式展示分享数据的原因。而普通app则只能处理并没有什么规范可言的系统提供的分享,随便来几个大家感受下……
share contents from 3rd app---> 我正在看【Stone】,分享给你,一起看吧! http://stone.netease.com/stone/download
(这个是来自UC浏览器的分享,算是很正常的也是最常见的一类)
share contents from 3rd app---> http://app.ichengzivr.com/video/share-2880-40.html
(这个是来自华为自带浏览器的,只有一个链接地址,不就是没有标题嘛可以接受)
share contents from 3rd app---> 我刚在网易新闻看到这个,快来围观:
【冷空气影响慢慢结束 接下去一天比一天暖和】
https://c.m.163.com/news/a/CE1ARK9K04098FC3.html?spss=newsapp&spsw=1
更深度内容,更有趣网友,尽在 网易新闻
http://www.163.com/newsapp
(这个是来自网易新闻客户端的,比别人多了一部分广告)
share contents from 3rd app---> 想看更多合你口味的内容,马上下载 今日头条
http://app.toutiao.com/news_article/?utm_source=link
【曾荫权身穿囚衣转至监狱服刑 72岁高龄将在囚房打工】
http://t.m.youth.cn/transfer/toutiao/url/picture.youth.cn/qtdb/201702/t20170224_9167724.htm?tt_group_id=6390509684017873154&tt_from=android_share&utm_medium=toutiao_android&utm_campaign=client_share
(瓦册则法克,搬出我的尼克扬标准问号脸,今日头条你都把自家广告放前面了!我服!)
各位看官也大致感受到这个充满“恶意”的世界了,拼接规则乱暂且不说想办法还是可以处理的,于是仿照钉钉、易信(安卓版),只是把分享数据当做普通文本消息发送给用户,实现方法很简单大致是,首先是写一个处理分享内容的activity,关键代码如下
private void handleShare() {
// Get intent, action and MIME type
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
handleSendText(sharedText); // Handle text being sent
} else if (type.startsWith("image/")) {
Uri imageUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); // Handle single image being sent
//由于分享图片不是关键,不多说了...
}
}
}
然后在清单文件注册
<activity
android:name=".module.share.activity.HandleSystemShareActivity"
android:label="@string/share_to_stone">
<!--接收单张图片分享-->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<!--接收普通文本分享-->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
其他具体细节可以参考http://www.cnblogs.com/xyzlmn/p/3444376.html
做出来之后效果大概是这样子
跟普通消息没半点差别,领导看了当然不同意,问怎么不做成微信的样子,起码放个图片做下样式能看的出来是个分享消息吧?作为一个合格的程序员(严肃脸),当然不能说自己实现不了,也不可能告诉领导微信人家用户量大有话语权别人都是乖乖接了他们家sdk的。但现实就是并没有哪个应用分享内容过来的时候会带给你一个图片,要自定义样式得有个图片啊,怎么办呢。。。我们机智的产品经理提醒了我,可以用网站的favicon啊,恩,是个可行的方法。
至于什么是favicon
Favicon 收藏夹图标,是其可以让浏览器的收藏夹中除显示相应的标题外,还以图标的方式区别不同的网站。收藏夹图标就是出现在浏览器地址栏左侧的那个小图标。收藏夹图标,也作网站头像。
就是这个东西
如何获取favicon呢?逆向思维,先研究如何设置的。大致有两种方法:
1.网站根目录下设置favicon.ico文件;
比如http://blog.csdn.net/favicon.ico
2.HTML代码的head标签里面加入代码:
<link href="icon文件url" rel="shortcut icon">
或者
<link rel="shortcut icon" href="icon文件url">
其中第一种方法是最常用的,第二种则较多的用于特殊页面设置不同的图标。知道如何设置favicon,获取就有办法了。解析html相对复杂又耗时,而且大部分网站都采用第一种方案,所以优先获取网站根目录下的favicon文件,如果没有再解析html的头文件。
思路相对简单,但是实现过程中遇到各种坑,首先来看根据url获取根目录
用正则从分享内容中拆分url和标题
//用于匹配url的正则表达式
private static final String URL_REGULAR = "\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^[:punct:]\\s]|/)))";
Pattern p = Pattern.compile(URL_REGULAR, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(sharedText);
if (m.find()) {
String shareUrl = m.group();
String shareTitle = sharedText.replaceAll("\\n", "").substring(0, sharedText.indexOf(shareUrl));
}
正则匹配得到分享链接之后拼接根目录地址
URL url = new URL(urlString);
// 保证从域名根路径搜索
String iconUrl = url.getProtocol() + "://" + url.getHost() + "/favicon.ico";
但其实这样得到的很可能不是根目录的地址,原因有两个,一个是有的url有重定向的情况,另外一种情况是由于分享是从移动端发起的很多的网站对wap做了适配,典型的网址类似是http://m.#host#.com,但是wap端的网站不一定会配置有favicon,比如淘宝https://m.taobao.com/favicon.ico返回404而https://www.taobao.com/favicon.ico则是正确的favicon地址。那么先看第一种重定向的情况
private String getFinalUrl(String urlString) {
HttpURLConnection connection = null;
try {
connection = getConnection(urlString);
connection.connect();
// 处理301/302重定向
if (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM
|| connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) {
String location = connection.getHeaderField("Location");
if (!location.contains("http")) {
location = urlString + "/" + location;
}
LogUtil.i(TAG, "handle redirect before:" + urlString + "\n after: " + location);
return location;
}
} catch (Exception e) {
LogUtil.e(TAG, "handle redirect timeout,return origin url:" + urlString);
} finally {
if (connection != null)
connection.disconnect();
}
return urlString;
}
然后是处理wap网站根目录不存在favicon的情况
/**
* 处理某些网站专门针对wap适配的手机版例如淘宝https://m.taobao.com/#index
* http://dynamic.m.tuniu.com/
*
* @param urlString
* @return
*/
private String handleWapUrl(String urlString) throws MalformedURLException {
if (!TextUtils.isEmpty(urlString) && urlString.contains("://m.")) {
return urlString.replaceFirst("://m.", "://www.");
}
if (!TextUtils.isEmpty(urlString) && urlString.contains(".m.")) {
URL url = new URL(urlString);
return url.getProtocol() + "://www." + urlString.substring(urlString.indexOf(".m."));
}
return urlString;
}
当然处理wap网站根目录不存在favicon这种情况要放在正常处理之后,总的来说就是这样的
public String getIconUrlString(String urlString) throws MalformedURLException {
urlString = getFinalUrl(urlString);
URL url = new URL(urlString);
String iconUrl = url.getProtocol() + "://" + url.getHost() + "/favicon.ico";
if (hasFavicon(iconUrl)) {
return iconUrl;
} else {
//大部分网站做了移动端访问的适配,但wap类型的网址不一定有favicon
String iconUrlNew = handleWapUrl(iconUrl);
LogUtil.i(TAG, "handle wap url,before: " + iconUrl + "\n after: " + iconUrlNew);
if (!TextUtils.equals(iconUrl, iconUrlNew) && hasFavicon(iconUrlNew)) {
return iconUrlNew;
}
}
return getIconUrlByRegex(urlString);
}
判断根目录是否存在favicon的方法可以采用返回码是200且contentLength>0的方法
return HttpURLConnection.HTTP_OK == connection.getResponseCode() && connection.getContentLength() > 0;
那么到此为止从网站根目录下面取favicon的方案算是讲完了,正常的网站都是可以取到的,当然也还是存在一些奇葩的网站,比如大优酷http://www.youku.com/favicon.ico根本就不是favicon地址,浏览器右键审查源文件可以看到
<head>
<meta charset="utf-8">
<meta name="title" content="优酷-中国领先视频网站,提供视频播放,视频发布,视频搜索 - 优酷视频" />
...
<link rel="Shortcut Icon" href="//static.youku.com/v1.0.166/index/img/favicon.ico" />
...
</head>
没错就是这句
<link rel="Shortcut Icon" href="//static.youku.com/v1.0.166/index/img/favicon.ico" />
那么如何拿到favicon地址呢,首先要获取到head标签里的内容,关键代码如下
HttpURLConnection connection = getConnection(urlString);
connection.connect();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
StringBuilder headBuilder = new StringBuilder();
while ((line = reader.readLine()) != null) {
if (!line.contains("</head>")) {
headBuilder.append(line);
} else {
break;
}
}
return headBuilder.toString();
然后通过正则匹配含有shortcut icon的标签link标签,由于html是不区分大小写的并且rel和href和互换位置所以正则可以这样写
private static final Pattern[] ICON_PATTERNS = new Pattern[]{
Pattern.compile("rel=[\"']shortcut icon[\"'][^\r\n>]+?((?<=href=[\"']).+?(?=[\"']))", Pattern.CASE_INSENSITIVE),
Pattern.compile("((?<=href=[\"']).+?(?=[\"']))[^\r\n<]+?rel=[\"']shortcut icon[\"']", Pattern.CASE_INSENSITIVE)
};
匹配到favicon的url之后还有最后一个坑,那就是返回的有可能是相对路径,所以还要再做下处理
//如果是完整的链接地址则直接返回
if (iconUrl.contains("http"))
return iconUrl;
//判断是否为相对路径或根路径
if (iconUrl.charAt(0) == '/') {
URL url = new URL(urlString);
//不包含协议的情况
if (iconUrl.startsWith("//")) {
iconUrl = url.getProtocol() + ":" + iconUrl;
} else {
iconUrl = url.getProtocol() + "://" + url.getHost() + iconUrl;
}
} else {
iconUrl = urlString + "/" + iconUrl;
}
return iconUrl;
到这里我们就成功获取到了嵌入在head标签里面的favicon,OK大功告成!
值得一提的是,因为上面两种获取favicon的途径都需要网络请求,有网络请求则意味着用户要等待,所以交互上要友好的做下处理。最后的成果大致如图,与前面的对比还是很强烈的。
细心地看官会发现:哎,怎么有的网站favicon模糊有的清晰咧?像上图中搜狐的明显就清晰嘛。这个问题我大致做了下调查,favicon其实是有尺寸规范的,比如window平台各浏览器最常见的尺寸是1616或者3232,而在MacOS/Safari上尺寸则可能是196196,在Google TV上则可能是9696,所以我尝试给HttpUrlConnection设置不同类型的User-Agent,但可气的是并不管用,都有点怀疑是不是各大网站偷懒了,也可能是我没找对办法,还望大前端的各位指点一下。
参考文章
【Android应用中实现系统“分享”接口 】http://blog.csdn.net/lowprofile_coding/article/details/37656255
【网页favicon.ico图标设置 】http://blog.csdn.net/zhizaibide1987/article/details/42001955
【获取网站图标Icon】 http://www.cnblogs.com/luguo3000/p/3767380.html
【Favicon 的尺寸】 https://www.coder-note.com/questions/2268204/favicon-dimensions