目前,社交網絡概念正火。而手機最初設計的目的正是讓人們進行通信。人人網作為中國最大的社交網站,用戶數量眾多,本文通過一個簡單的小程序:“我在聽”向大傢展示renren api的使用。
首先介紹一下“我在聽”。功能:在用戶聽歌時,在不需要用戶進行額外操作的情況下,根據用戶正在聽的曲目,以發狀態的形式同步至人人網。在安裝完“我在聽”之後,點擊使用人人網登錄,輸入賬號密碼,登錄成功後,選擇是否自動同步,如果此時用戶打開瞭網絡,那麼隻要用戶通過自帶的播放器聽歌,就會自動發佈狀態,例如:我在聽張國榮的《倩女幽魂》。
下面開始介紹開發過程;
首先,在人人 api頁面http://dev.renren.com/ 裡先登錄,然後創建一個android應用。填寫完表單,創建完成後,可以獲得人人給你的唯一標志:
應用ID:xxxxxxx
API Key:xxxxxxxxxxxxxxxxxxxxxxx
Secret Key:xxxxxxxxxxxxxxxxxxxxxx
這三串字符串用於標志你的應用。
下面介紹如何獲取用戶當前正在聽的歌的信息:
當系統默認的播放器開始播放下一首歌時,會發出一個廣播(intent中包含歌曲名,藝術傢等信息),我們隻要定義一個接收這個廣播的廣播接收器,並且從intent中抽取出需要的信息即可。
AndroidManifest.xml
[html] view plaincopyprint?
1. <span style="font-size:16px;"><?xml version="1.0" encoding="utf-8"?>
2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3. package="com.renoqiu"
4. android:versionCode="1"
5. android:versionName="1.0" >
6. <uses-sdk android:minSdkVersion="10" />
7. <uses-permission android:name="android.permission.INTERNET" />
8. <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
9. <application
10. android:icon="@drawable/ic_launcher"
11. android:label="@string/app_name" >
12. <activity
13. android:name=".IamListenActivity"
14. android:label="@string/app_name" >
15. </activity>
16. <activity
17. android:name=".SettingActivity"
18. android:label="@string/app_name" >
19. <intent-filter>
20. <action android:name="android.intent.action.MAIN" />
21. <category android:name="android.intent.category.LAUNCHER" />
22. </intent-filter>
23. </activity>
24. <receiver android:name=".MusicBroadcastReceiver">
25. <intent-filter>
26. <action android:name="com.android.music.metachanged"></action>
27. </intent-filter>
28. </receiver>
29. <service android:name=".PushStatusService" >
30. <intent-filter>
31. <action android:name="com.renoqiu.pushstatus" />
32. </intent-filter>
33. </service>
34. </application>
35. </manifest>
36. </span>
根據main.xml可知,我們定義瞭類MusicBroadcastReceiver捕捉action名為com.android.music.metachanged的廣播。
src/com/renoqiu/ MusicBroadcastReceiver.java
[java] view plaincopyprint?
1. <span style="font-size:16px;">package com.renoqiu;
2.
3. import android.content.BroadcastReceiver;
4. import android.content.Context;
5. import android.content.Intent;
6. import android.os.Bundle;
7.
8. public class MusicBroadcastReceiver extends BroadcastReceiver {
9. private static final Object SMSRECEIVED = "com.android.music.metachanged";
10. @Override
11. public void onReceive(Context context, Intent intent) {
12. if(intent.getAction().equals(SMSRECEIVED)){
13. String trackName=intent.getStringExtra("track");
14. String artist=intent.getStringExtra("artist");
15.
16. Intent pushStatusIntent = new Intent();
17. pushStatusIntent.setAction("com.renoqiu.pushstatus");
18. Bundle myBundle = new Bundle();
19. myBundle.putString("trackName", trackName);
20. myBundle.putString("artist", artist);
21. pushStatusIntent.putExtras(myBundle);
22. context.startService(pushStatusIntent);
23. }
24. }
25. }
26. </span>
在類MusicBroadcastReceiver中我們抽取瞭歌曲名和藝術傢名,並且調用context.startService()方法創建瞭一個service。
src/com/renoqiu/ PushStatusService.java
[java] view plaincopyprint?
1. <span style="font-size:16px;">package com.renoqiu;
2.
3. import android.app.Service;
4. import android.content.Context;
5. import android.content.Intent;
6. import android.content.SharedPreferences;
7. import android.net.ConnectivityManager;
8. import android.net.NetworkInfo;
9. import android.os.Bundle;
10. import android.os.Handler;
11. import android.os.IBinder;
12. import android.os.Message;
13. import android.widget.Toast;
14.
15. public class PushStatusService extends Service {
16. private Handler handler;
17. private com.renoqiu.LooperThread thread;
18. private SharedPreferences sharedPreferences;
19. private boolean syncFlag;
20. private ConnectivityManager cm;
21. private NetworkInfo ni;
22.
23. @Override
24. public IBinder onBind(Intent arg0) {
25. return null;
26. }
27. private boolean checkNet() {
28. cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
29. if (cm == null) {
30. return false;
31. }
32. ni = cm.getActiveNetworkInfo();
33. if (ni == null || !ni.isAvailable()) {
34. return false;
35. }
36. return true;
37. }
38. @Override
39. public void onStart(Intent intent, int startId) {
40. super.onStart(intent, startId);
41.
42. syncFlag = sharedPreferences.getBoolean("syncFlag",false);
43. if(syncFlag && checkNet()){
44. if(intent != null){
45. Bundle myBundle = intent.getExtras();
46. String trackName = myBundle.getString("trackName");
47. String artist = myBundle.getString("artist");
48. String accessToken = sharedPreferences.getString("accessToken","");
49.
50. if(accessToken != null && !accessToken.equals("")){
51. String requestMethod = "status.set";
52. //接口名稱
53. String url = StatusPublishHelper.API_URL;
54. String secretKey = StatusPublishHelper.SECRET_KEY;
55. if(artist == null || artist.equals("")){
56. artist = "xxx";
57. }
58. if(trackName == null || trackName.equals("")){
59. trackName = "xxx";
60. }
61. String message = "我在聽" + artist + "的《" + trackName + "》。\r\n 通過我在聽發佈!";
62. thread = new LooperThread(handler, requestMethod, "1.0", url, accessToken, message, secretKey);
63. thread.start(); /* 啟動線程 */
64. }else{
65. Toast.makeText(PushStatusService.this, "請登陸!", Toast.LENGTH_SHORT).show();
66. SharedPreferences.Editor editor = sharedPreferences.edit();
67. editor.putBoolean("syncFlag", false);
68. editor.commit();
69. }
70. }
71. }
72. }
73.
74. @Override
75. public void onCreate() {
76. super.onCreate();
77. sharedPreferences = getSharedPreferences("shared", MODE_PRIVATE);
78.
79. handler = new Handler() {
80. @Override
81. public void handleMessage(Message msg) {
82. switch (msg.what) {
83. case 0:
84. if((Integer)msg.obj != 1){
85. Toast.makeText(PushStatusService.this, "同步失敗!請檢查網絡是否打開…", Toast.LENGTH_SHORT).show();
86. }else{
87. Toast.makeText(PushStatusService.this, "同步成功!", Toast.LENGTH_SHORT).show();
88. }
89. break;
90. }
91. }
92. };
93. }
94. }
95. </span>
在PushStatusService中,首先進行一系列檢查,例如:網絡是否已經打開,用戶是否已經登錄,是否開啟同步等信息。其中用到瞭sharedPreferences保存信息。如果條件都滿足那麼將新建一個線程去通過人人api發狀態,其中就需要使用到之前創建應用時所得到的key,在介紹具體如何狀態之前先接著介紹一下人人的api。要通過人人發送狀態首先必須通過人人的登錄認證:OAuth2.0,詳情見:http://wiki.dev.renren.com/wiki/Authentication
我們的登錄流程開始於通過內嵌在IamListenActivity中的Webkit訪問人人OAuth 2.0的Authorize Endpoint:
https://graph.renren.com/oauth/authorize?client_id=YOUR_API_KEY&response_type=token&redirect_uri=YOUR_CALLBACK_URL&display=touch&scope=status_update。
client_id:必須參數。在開發者中心註冊應用時獲得的API Key。
response_type:必須參數。客戶端流程,此值固定為“token”。當用戶登錄成功,瀏覽器會被重定向到YOUR_CALLBACK_URL,並且帶有參數Access Token。這個Access Token可以標志登錄用戶,避免需要多次輸入用戶名密碼。此處我們會把accesstoken保存在sharedPreferences中,下次需要使用時,直接從sharedPreferences中獲取。
redirect_uri:登錄成功,流程結束後要跳轉回得URL。redirect_uri所在的域名必須在開發者中心註冊應用後,填寫在編輯屬性選項卡中填寫到服務器域名中,人人OAuth2.0用以檢查跳轉的合法性。
如果用戶已經登錄,人人OAuth 2.0會校驗存儲在用戶瀏覽器中的Cookie。如果用戶沒有登錄,人人OAuth 2.0會為用戶展示登錄頁面,讓用戶輸入用戶名和密碼:
display=touch:一般的智能手機都是這個選項,
scope=status_update:表示我們會用到更新狀態的功能。
src/com/renoqiu/ IamListenActivity.java
[java] view plaincopyprint?
1. <span style="font-size:16px;">package com.renoqiu;
2.
3. import java.net.URLDecoder;
4. import android.app.Activity;
5. import android.content.SharedPreferences;
6. import android.os.Bundle;
7. import android.webkit.WebSettings;
8. import android.webkit.WebView;
9. import android.webkit.WebViewClient;
10. import android.widget.Toast;
11.
12. public class IamListenActivity extends Activity {
13. private WebView webView;
14. private String accessToken = null;
15. public void onCreate(Bundle savedInstanceState) {
16. super.onCreate(savedInstanceState);
17. setContentView(R.layout.main);
18. webView = (WebView) findViewById(R.id.web);
19. WebSettings settings = webView.getSettings();
20. settings.setJavaScriptEnabled(true);
21. settings.setSupportZoom(true);
22. settings.setBuiltInZoomControls(true);
23. webView.loadUrl(StatusPublishHelper.AUTHURL);
24. webView.requestFocusFromTouch();
25. WebViewClient wvc = new WebViewClient() {
26. @Override
27. public void onPageFinished(WebView view, String url) {
28. super.onPageFinished(view, url);
29. //人人網用戶名和密碼驗證通過後,刷新頁面時即可返回accessToken
30. String reUrl = webView.getUrl();
31. if (reUrl != null && reUrl.indexOf("access_token") != -1) {
32. //截取url中的accessToken
33. int startPos = reUrl.indexOf("token=") + 6;
34. int endPos = reUrl.indexOf("&expires_in");
35. accessToken = URLDecoder.decode(reUrl.substring(startPos, endPos));
36. //保存獲取到的accessToken
37. //share.saveRenrenToken(accessToken);
38. Toast.makeText(IamListenActivity.this, "驗證成功,設置同步後。\n聽歌時就能自動傳狀態哦。:)", Toast.LENGTH_SHORT).show();
39. SharedPreferences settings = (SharedPreferences)getSharedPreferences("shared", MODE_PRIVATE);
40. SharedPreferences.Editor editor = settings.edit();
41. editor.putString("accessToken", accessToken);
42. editor.putBoolean("syncFlag", false);
43. editor.commit();
44. finish();
45. }
46. }
47. };
48. webView.setWebViewClient(wvc);
49. }
50. }
51. </span>
res/layout/main.xml
[html] view plaincopyprint?
1. <span style="font-size:16px;"><?xml version="1.0" encoding="utf-8"?>
2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3. android:layout_width="fill_parent"
4. android:layout_height="fill_parent"
5. android:orientation="vertical" >
6.
7. <WebView
8. android:id="@+id/web"
9. android:layout_width="match_parent"
10. android:layout_height="match_parent" />
11. </LinearLayout>
12. </span>
由代碼可知,我們通過webkit訪問Authorize Endpoint後,從返回的鏈接中抽取瞭accesstoken,並且保存起來瞭。有瞭access token之後,我們就可以通過renren的api,進行更新狀態的操作瞭,下面的LooperThread就是用於更新狀態,並且給出用戶反饋的類。
“為瞭確保應用與人人API 服務器之間的安全通信,防止Secret Key盜用,數據篡改等惡意攻擊,人人API服務器使用瞭簽名機制(即sig參數)來認證應用。簽名是由請求參數和應用的私鑰Secret Key經過MD5加密後生成的字符串。應用在調用人人API之前,要計算出簽名,並追加到請求參數中。“(關於簽名的計算規則參見:http://wiki.dev.renren.com/wiki/Calculate_signature)
src/com/renoqiu/ LooperThread.java
[java] view plaincopyprint?
1. <span style="font-size:16px;">package com.renoqiu;
2.
3. import java.util.ArrayList;
4. import java.util.List;
5.
6. import org.apache.http.HttpResponse;
7. import org.apache.http.NameValuePair;
8. import org.apache.http.client.entity.UrlEncodedFormEntity;
9. import org.apache.http.client.methods.HttpPost;
10. import org.apache.http.impl.client.DefaultHttpClient;
11. import org.apache.http.message.BasicNameValuePair;
12. import org.apache.http.protocol.HTTP;
13. import org.apache.http.util.EntityUtils;
14. import org.json.JSONObject;
15.
16. import android.os.Handler;
17. import android.os.Message;
18. import android.util.Log;
19.
20. public class LooperThread extends Thread{
21. private String requestMethod;
22. private String v;
23. private String url;
24. private String accessToken;
25. private String status;
26. private String secretKey;
27. private Handler fatherHandler;
28. public LooperThread(Handler fatherHandler, String requestMethod, String v, String url, String accessToken, String message, String secretKey) {
29. this.requestMethod = requestMethod;
30. this.v = v;
31. this.url = url;
32. this.accessToken = accessToken;
33. this.status = message;
34. this.secretKey = secretKey;
35. this.fatherHandler = fatherHandler;
36. }
37.
38. public void run() {
39. Message msg = new Message();
40. msg.obj = updateStatus(status);
41. msg.what = 0;
42. fatherHandler.sendMessage(msg);
43. }
44. public int updateStatus(String status) {
45. int success = 0;
46. //生成簽名 字典序排列
47. StringBuilder sb = new StringBuilder();
48. sb.append("access_token=").append(accessToken)
49. .append("format=").append("JSON")
50. .append("method=").append(requestMethod)
51. .append("status=").append(status)
52. .append("v=").append(v)
53. .append(secretKey);
54. String sig = StatusPublishHelper.getMD5(sb.toString());
55.
56. HttpPost httpRequest = new HttpPost(url);
57. List<NameValuePair> params = new ArrayList<NameValuePair>();
58. params.add(new BasicNameValuePair("access_token", accessToken));
59. params.add(new BasicNameValuePair("method", requestMethod));
60. params.add(new BasicNameValuePair("v", v));
61. params.add(new BasicNameValuePair("status", status));
62. params.add(new BasicNameValuePair("format", "JSON"));
63. params.add(new BasicNameValuePair("sig", sig));
64. try {
65. httpRequest.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
66. HttpResponse httpResponse = new DefaultHttpClient().execute(httpRequest);
67. if (httpResponse.getStatusLine().getStatusCode() == 200){
68. String result = EntityUtils.toString(httpResponse .getEntity());
69. JSONObject json = new JSONObject(result);
70. success = (Integer)json.get("result");
71. Log.v("org.reno", result);
72. }
73. }catch (Exception e){
74. return success;
75. }
76.
77. return success;
78. }
79. }
80. </span>
上面代碼中,首先計算出所有參數以字典序升序排列後,拼接在一起後的md5值作為簽名。然後向http://api.renren.com/restserver.do發送post請求,其中包括之前獲得的access_token,所調用的方法名(此處我們調用的是更新狀態的方法名,關於各種api詳見:http://wiki.dev.renren.com/wiki/API),及方法的參數,此處包括版本,狀態內容,返回類型(此處為json),最後是簽名。
params.add(newBasicNameValuePair("access_token", accessToken));
params.add(newBasicNameValuePair("method", requestMethod));
params.add(newBasicNameValuePair("v", v));
params.add(newBasicNameValuePair("status", status));
params.add(newBasicNameValuePair("format", "JSON"));
params.add(newBasicNameValuePair("sig", sig));
然後等待服務器的回復,並且解析json格式的數據,判斷是否發送成功。
到此,基本的發狀態的過程就結束瞭。
下面的類用於提供用戶登陸以及讓用戶選擇是否開啟自動同步。
src/com/renoqiu/ SettingActivity.java
[java] view plaincopyprint?
1. <span style="font-size:16px;">package com.renoqiu;
2.
3. import android.app.Activity;
4. import android.content.Intent;
5. import android.content.SharedPreferences;
6. import android.os.Bundle;
7. import android.view.View;
8. import android.view.View.OnClickListener;
9. import android.widget.Button;
10. import android.widget.CompoundButton;
11. import android.widget.Toast;
12. import android.widget.CompoundButton.OnCheckedChangeListener;
13. import android.widget.ToggleButton;
14.
15. public class SettingActivity extends Activity {
16. private ToggleButton syncToggleButton;
17. private Button loginBtn;
18. private SharedPreferences sharedPreferences;
19. @Override
20. protected void onCreate(Bundle savedInstanceState) {
21. super.onCreate(savedInstanceState);
22. setContentView(R.layout.setting);
23. syncToggleButton = (ToggleButton)findViewById(R.id.syncToggleButton);
24. sharedPreferences = (SharedPreferences)getSharedPreferences("shared", MODE_PRIVATE);
25. boolean syncFlag = sharedPreferences.getBoolean("syncFlag",false);
26. syncToggleButton.setChecked(syncFlag);
27. syncToggleButton.setOnCheckedChangeListener(new OnCheckedChangeListener(){
28. @Override
29. public void onCheckedChanged(CompoundButton buttonView,
30. boolean isChecked) {
31. if(isChecked == false){
32. SharedPreferences.Editor editor = sharedPreferences.edit();
33. editor.putBoolean("syncFlag", isChecked);
34. editor.commit();
35. }else{
36. String accessToken = sharedPreferences.getString("accessToken","");
37. if(accessToken != null && !accessToken.equals("") ){
38. SharedPreferences.Editor editor = sharedPreferences.edit();
39. editor.putBoolean("syncFlag", isChecked);
40. editor.commit();
41. }else{
42. syncToggleButton.setChecked(false);
43. Toast.makeText(SettingActivity.this, "請先登陸!", Toast.LENGTH_SHORT).show();
44. }
45.
46. }
47. }
48. });
49. loginBtn = (Button)findViewById(R.id.loginBtn);
50. loginBtn.setOnClickListener(new OnClickListener(){
51. @Override
52. public void onClick(View arg0) {
53. Intent intent = new Intent(SettingActivity.this, IamListenActivity.class);
54. startActivity(intent);
55. }});
56. }
57. }
58. </span>
res/layout/setting.xml
[html] view plaincopyprint?
1. <span style="font-size:16px;"><?xml version="1.0" encoding="utf-8"?>
2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3. android:layout_width="fill_parent"
4. android:layout_height="fill_parent"
5. android:orientation="vertical" >
6. <Button
7. android:id="@+id/loginBtn"
8. android:layout_width="wrap_content"
9. android:layout_height="wrap_content"
10. android:text=""
11. android:background="@drawable/btn_login"
12. android:layout_marginTop="10dp"/>
13.
14. <RelativeLayout
15. android:id="@+id/relativeLayout1"
16. android:layout_width="match_parent"
17. android:layout_height="wrap_content" >
18.
19. <TextView
20. android:id="@+id/toggleTextView"
21. android:layout_width="wrap_content"
22. android:layout_height="wrap_content"
23. android:layout_alignParentLeft="true"
24. android:layout_alignParentTop="true"
25. android:layout_marginTop="20dp"
26. android:text="@string/toggleSync" />
27.
28. <ToggleButton
29. android:id="@+id/syncToggleButton"
30. android:layout_width="wrap_content"
31. android:layout_height="wrap_content"
32. android:layout_alignParentTop="true"
33. android:layout_marginTop="10dp"
34. android:layout_marginLeft="10dp"
35. android:layout_toRightOf="@+id/toggleTextView" />
36.
37. </RelativeLayout>
38.
39. </LinearLayout>
40. </span>
最後一個類包含瞭各種鏈接常量,和把字符串轉換為md5的方法:
src/com/renoqiu/ StatusPublishHelper.java
[java] view plaincopyprint?
1. <span style="font-size:16px;">package com.renoqiu;
2.
3. import java.security.MessageDigest;
4.
5. public class StatusPublishHelper {
6. // 你的應用ID
7. public static final String APP_ID = "xxxxx";
8. // 應用的API Key
9. public static final String API_KEY = "xxxxxxxxxxxxxxxxxxxxxxx";
10. // 應用的Secret Key
11. public static final String SECRET_KEY = "xxxxxxxxxxxxxxxx";
12.
13. public static final String API_URL = "http://api.renren.com/restserver.do";
14. public static final String AUTHURL = "https://graph.renren.com/oauth/authorize?client_id="
15. + API_KEY +"&response_type=token"
16. + "&redirect_uri=http://www.renoqiu.com/iamlisten.html&display=touch"
17. + "&scope=status_update";
18. public static String getMD5(String s) {
19. try {
20. MessageDigest md5 = MessageDigest.getInstance("MD5");
21.
22. byte[] byteArray = s.getBytes("UTF-8");
23. byte[] md5Bytes = md5.digest(byteArray);
24.
25. StringBuffer hexValue = new StringBuffer();
26.
27. for (int i = 0; i < md5Bytes.length; i++) {
28. int val = ((int) md5Bytes[i]) & 0xff;
29. if (val < 16)
30. hexValue.append("0");
31. hexValue.append(Integer.toHexString(val));
32. }
33.
34. return hexValue.toString();
35.
36. } catch (Exception e) {
37. e.printStackTrace();
38. return null;
39. }
40. }
41. }
42. </span>
下圖為測試使用的效果。
SourceCode下載鏈接:
https://github.com/renoqiu/IamListening
需要註意的是:下載的源代碼並不能直接使用,讀者需要自行修改IamListening/src/com/renoqiu/StatusPublishHelper.java類下的APP_ID, API_KEY, SECRET_KEY 這三個常量為你申請的應用的對應的值後,就可以正常使用瞭。
http://code.google.com/p/iamlisten/downloads/list可以從這裡現在一個筆者編譯好的apk文件,進行測試。
摘自 北京大學-Google Android實驗室