作者:贺小令
人人网(www.renren.com)前身为校内网,是中国最大、最受用户欢迎的网络SNS平台。人人网目前针对不同领域的开发者,提供了相应的教程和文档,目前主要分为“站内应用开发”、“第三方网站接入”、“移动客户端接入”和“桌面客户端接入”四大模块。以下结合自己实际做过的一个项目(人人好友电话簿)介绍基于Android开发的移动客户端接入基本流程。
手机等移动设备的客户端应用(如手机游戏、实用工具等)接入人人网,可以使用人人网帐号登录移动客户端,并利用人人网开放平台提供的社交图谱(Social Graph)和传播渠道,增进用户与好友的交互,提升使用体验,并获得广泛传播。
Android手机客户端接入人人网,可以有两种实现方式:
第一种是直接使用人人网开放平台提供的各种接口,如用作验证和授权的OAuth 2.0,提供数据的底层Rest API,以及嵌入各种Widget。关于OAuth 2.0详细协议介绍,请参考https://datatracker.ietf.org/doc/draft-ietf-oauth-v2/,后文会介绍在人人中的相关用法。
第二种是使用人人网开放平台官方封装的开源Android SDK。 人人网最新版的Android SDK实际上是将OAuth 2.0、Rest API等平台提供的底层接口封装起来而已。
在我的项目中我采用了第一种方式,即直接使用人人网开放平台提供的各种接口,这种方式比较简单,自己可以随心所欲的发挥。第二种方式,我看过人人提供的SDK,比较晦涩难懂。如有兴趣,可以自己去研究。
整体流程如下图:
第一步:注册一个人人账户,如果有了,跳过此步。
第二步:用自己的账号密码登陆到人人网,然后跳转到http://dev.renren.com/app,点击创建客户端应用按钮,并按相应的流程填写完相应信息,你就可以提交应用审核。完成后你将得到:API Key和Secret Key,如下图:
API Key就是人人OAuth2.0中的“client_id”,Secret Key就是“client_secret”。
第三、四步:
人人网开放平台提供了上述的OAuth2.0验证与授权流程以支持不同类型的应用,包括:网站、站内应用、手机客户端和桌面客户端。
由于OAuth 2.0协议定义的桌面客户端的授权流程,用户体验方面过于复杂,所以桌面客户端应用可以在应用中嵌入浏览器控件(很多框架都支持嵌入浏览器,例如:.NET、AIR、Cocoa等)使用客户端流程。
由于大部分桌面客户端软件是没有后端没有Web服务器支持,没办法提供一个'redirect_uri',所以人人网为没有Web服务器的客户端应用提供了一个通用的URL:http://graph.renren.com/oauth/login_success.html。
流程如下:
在应用中嵌入一个浏览器控件,并使用客户端流程定向控件到人人OAuth 2.0 Authorize Endpoint(https://graph.renren.com/oauth/authorize):
https://graph.renren.com/oauth/authorize?client_id=YOUR_API_KEY&redirect_uri=http://graph.renren.com/oauth/login_success.html
经过用户验证、应用授权,人人OAuth2.0将把浏览器控件定向导'redirect_uri'(http://graph.renren.com/oauth/login_success.html),并在URI Fragment中追加Access Token:http://graph.renren.com/oauth/login_success.html#access_token=...当应用发现浏览器的控件的URL跳转到这个URL上时,从URL中解析出Access Token。
在Android中加载html用WebView控件,由于Android主要是触屏,因此需要加display=touch参数,完整的URL为
https://graph.renren.com/oauth/authorize?client_id=05d3794614f244c39e300c65f5f68a9e&response_type=token&display=touch&redirect_uri=http://graph.renren.com/oauth/login_success.html
登陆授权流程如下:
主要代码和注释如下:
RenrenLoginActivity.java
import android.app.Activity;
……
/**
* @author hexiaoling
*/
public class RenrenLoginActivity extends Activity {
public final static String TAG = "RenrenLoginActivity";
private WebView renrenLoginWebView; // WebView 控件,用于显示从人人网请求得到html授权页面
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.renren_login_web_view);
renrenLoginWebView = (WebView) findViewById(R.id.renren_login_web_view); // 得到 WebView 控件
//对WebView进行设置(对JS的支持,对缩放的支持,对缓存模式的支持)
WebSettings webSettings = renrenLoginWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
// 根据client_id取得到人人服务器人人对你的应用授权,如果成功则返回人人网登陆页面的html文件,并在WebView控件上显示
// 此时用户需要输入自己人人账号的用户名、密码并点击登陆
renrenLoginWebView.loadUrl("https://graph.renren.com/oauth/authorize?"+
"client_id=05d3794614f244c39e300c65f5f68a9e&response_type=token"+
"&display=touch&redirect_uri=http://graph.renren.com/oauth/login_success.html");
renrenLoginWebView.setWebViewClient(new WebViewClient() {
//击网页里面的链接还是在当前的webview里跳转,不跳到浏览器那边
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();//让webview处理https请求
}
@Override
public void onPageFinished(WebView view, String url) {
String url0 = renrenLoginWebView.getUrl();
String access_token = "";
String expires_in = "";
Log.i(TAG, "URL = " + url0);
if(url0 != null) {
if(url0.contains("access_token=")) { // 从URL中解析得到 access_token
access_token = url0.substring(url0.indexOf("access_token=") + 13, url0.length() - 19);
try {
access_token = URLDecoder.decode(access_token, "utf-8"); // 制定为utf-8编码
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Log.i(TAG, "access_token = " + access_token);
}
if(url0.contains("expires_in=")) { // 从URL中解析得到 expires_in
expires_in = url0.substring(url0.indexOf("expires_in=") + 11, url0.length());
Log.i(TAG, "expires_in = " + expires_in);
}
RenrenUtil.access_token = access_token; // 将解析得到的 access_token 保存起来
RenrenUtil.expires_in = expires_in; // 将解析得到的 expires_in 保存起来
//输入用户名、密码登陆成功,进行页面跳转
if(RenrenUtil.access_token.length() != 0) {
Intent intent = new Intent(RenrenLoginActivity.this, RenrenFriendsActivity.class);
startActivity(intent);
}
}
super.onPageFinished(view, url);
}
});
}
}
下图表示加载人人授权应用的html登陆成果界面:
第五步:这里以访问我的人人好友列表为例,包括取得好友的名称和照片。主要流程如下:
①人人API利用不同功能的请求函数和相关参数,详细请参考http://wiki.dev.renren.com/wiki/API。此处以获得好友列表为例,其对应函数为friends.getFriends(得到用户的信息,支持批量获取),人人对friends.getFriends的描述和参数列表如下图:
下图表示将请求参数和Secret得到MD5值为签名,并组装请求参数核心代码:
String method = " friends.getFriends "; // 人人API中定义的方法,用于得到当前登录用户的好友列表。
int count = 30; // 得到用户数30个
List<String> param = new ArrayList<String>();
param.add("method=" + method);
param.add("v=1.0"); // 版本固定参数
param.add("access_token=" + RenrenUtil.access_token); //RenrenUtil.access_token 是在LoginActivity中已经保存的数据
param.add("format=JSON"); // 返回JSON数据
param.add("count=" + count); //得到用户数
String signature = getSignature(param, "55f436828286417ab9db7ea17ee9cbde"); //第二个参数为 Secret Key
List<BasicNameValuePair> paramList = new ArrayList<BasicNameValuePair>();
paramList.add(new BasicNameValuePair("sig", signature)); // 签名
paramList.add(new BasicNameValuePair("method", method));
paramList.add(new BasicNameValuePair("v", "1.0"));
paramList.add(new BasicNameValuePair("access_token", RenrenUtil.access_token));
paramList.add(new BasicNameValuePair("format", "JSON"));
paramList.add(new BasicNameValuePair("count", "" + count));
下图表示将请求参数和SecretKey加密得到它们的MD5值的算法如下:
/**
* 得到MD5签名
* @param paramList
* @param secret
* @return
*/
public String getSignature(List<String> paramList,String secret) {
Collections.sort(paramList);
StringBuffer buffer = new StringBuffer();
for (String param : paramList) {
buffer.append(param); //将参数键值对,以字典序升序排列后,拼接在一起
}
buffer.append(secret); //符串末尾追加上应用的Secret Key
try {//下面是将拼好的字符串转成MD5值,然后返回
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
StringBuffer result = new StringBuffer();
try {
for (byte b : md.digest(buffer.toString().getBytes("utf-8"))) {
result.append(Integer.toHexString((b & 0xf0) >>> 4));
result.append(Integer.toHexString(b & 0x0f));
}
} catch (UnsupportedEncodingException e) {
for (byte b : md.digest(buffer.toString().getBytes())) {
result.append(Integer.toHexString((b & 0xf0) >>> 4));
result.append(Integer.toHexString(b & 0x0f));
}
}
return result.toString();
} catch (java.security.NoSuchAlgorithmException ex) { }
return null;
}
②使用HTTP协议,Android已经对其做了很好的封装,使用起来很方便。使用将第①步得到的参数列表想http://api.renren.com/restserver.do发送Http请求,同时得到HttpResponse响应,判断响应代码是否为200,为200表示成功。然后解析JSON数据为相关对象,核心代码如下:
String returnValue = "0";
try {
HttpPost httpPost = new HttpPost("http://api.renren.com/restserver.do");
httpPost.setEntity(new UrlEncodedFormEntity(paramList, HTTP.UTF_8));// 添加请求参数到请求对象
HttpResponse httpResponse = httpClient.execute(httpPost);
if(httpResponse.getStatusLine().getStatusCode() == 200) { //为200表示执行成功
strResult = EntityUtils.toString(httpResponse.getEntity()); //得到返回数据(为JSON数据)
if(! strResult.contains("error_code")) {
renrenList = Renren.parseRenrenFromJson(strResult); //解析JSON数据为相应对象
returnValue = "1"; //定义返回标志
}
}
} catch (ClientProtocolException e) {
strResult = e.getMessage().toString();
e.printStackTrace();
} catch (IOException e) {
strResult = e.getMessage().toString();
e.printStackTrace();
} catch (Exception e) {
strResult = e.getMessage().toString();
e.printStackTrace();
}
return returnValue;
网络请求会比较耗时,因此建议使用多线程来处理。在Android中可以使用继承AsyncTask<String, Integer,String>类来实现多线程处理。
③解析JSON数据为人人对象方法:
/**
* 解析JSON数据为人人对象
* @param renrenJsonData
* @return
*/
public List<Renren> parseRenrenFromJson(String renrenJsonData) {
List<Renren> renrenList = new ArrayList<Renren>();
try {
JSONArray jsonArray = new JSONArray(renrenJsonData);
int length = jsonArray.length();
for (int i = 0; i < length; ++i) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
Renren renren = new Renren();
renren.setId(jsonObject.getString("id"));
renren.setName(jsonObject.getString("name"));
renren.setHeadurl(jsonObject.getString("headurl"));
renrenList.add(renren);
}
return renrenList;
} catch (JSONException e) { }
return null;
}
下图为JSON数据的数据结构:
人人类Renren.java
public class Renren {
protected String id;
protected String name;
protected String headurl;
省略get、set方法。
}
④在List中显示人人好友列表(图片、名称),并用Adapater来加载数据。由于请求friends.getFriends返回的只有图片的URL,因此图片还需要单独去请求,为了保障用户体验,图片采用异步加载。
public class FriendsAdapater extends BaseAdapter {
private AsyncImageLoader asyncImageLoader = new AsyncImageLoader(); // 异步获取图片
@Override
public int getCount() {
return renrenList == null ? 0 : renrenList.size();
}
@Override
public Object getItem(int position) {
return renrenList == null ? null :renrenList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
convertView = LayoutInflater.from(getApplicationContext()).inflate(R.layout.friend, null);
userName= (TextView) convertView.findViewById(R.id.username);
userImage = (ImageView) convertView.findViewById(R.id.userimage);
Renren renren = renrenList.get(position);
if (renren != null) {
convertView.setTag(renren.getId());
userImage.setTag(renren.getHeadurl());
userName.setText(renren.getName());
//异步加载图片并显示
Drawable cachedImage = asyncImageLoader.loadDrawable(renren, new ImageCallback() {
@Override
public void imageLoaded(Drawable imageDrawable, String imageUrl) {
ImageView imageView = (ImageView) friendsList.findViewWithTag(imageUrl);
if(imageView != null) {
imageView.setImageDrawable(imageDrawable);
}
}
});
if (cachedImage != null) {
userImage.setImageDrawable(cachedImage);
} else {//如果没有图片,就以一个载入的图片代替显示
userImage.setImageResource(R.drawable.icon);
}
}
return convertView;
}
}
异步加载图片类AsyncImageLoader.java
/**
* 异步加载图片类
* @author hexiaoling
*/
public class AsyncImageLoader {
private HashMap<String, SoftReference<Drawable>> imageCache; //缓存图片
public AsyncImageLoader() {
imageCache = new HashMap<String, SoftReference<Drawable>>();
}
public Drawable loadDrawable(final Renren renren, final ImageCallback imageCallback) {
final String imageId = renren.getId();
final String imageURL = renren.getHeadurl();
//判断缓存中是否已经存在图片,如果存在则直接返回
if (imageCache.containsKey(imageId)) {
SoftReference<Drawable> softReference = imageCache.get(imageId);
Drawable drawable = softReference.get();
if (drawable != null) {
return drawable;
}
}
final Handler handler = new Handler() {
public void handleMessage(Message message) {
imageCallback.imageLoaded((Drawable) message.obj, imageURL);
}
};
//开辟一个新线程去下载图片,并用Handler去更新UI
new Thread() {
@Override
public void run() {
Drawable drawable = loadImageFromUrl(imageURL);
imageCache.put(imageURL, new SoftReference<Drawable>(drawable));
Message message = handler.obtainMessage(0, drawable);
handler.sendMessage(message);
}
}.start();
return null;
}
//从URL下载图片
public static Drawable loadImageFromUrl(String url) {
URL m;
InputStream i = null;
try {
m = new URL(url);
i = (InputStream) m.getContent();
} catch (MalformedURLException e1) {
e1.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Drawable d = Drawable.createFromStream(i, "src");
return d;
}
//回调接口
public interface ImageCallback {
//回调函数
public void imageLoaded(Drawable imageDrawable, String imageUrl);
}
}
下图为运行结果:(因涉及好友真实信息,不将部分图片和文字加码,此结果仅表示按照此步骤运行正确)
另外需要注意的是,访问人人API需要访问网络,因此需要在添加网络访问授权
<uses-permissionandroid:name="android.permission.INTERNET" />
此文仅简单介绍了基于人人网API的Android开发的基本步骤,如需了解更多信息,请研究人人官网http://wiki.dev.renren.com/wiki/API。欢迎就上述问题进一步交流。
参考文献:http://wiki.dev.renren.com/wiki/API