android项目源码解析04:新浪微博客户端源码解析

本文主要介绍如何构建新浪微博客户端。以网上流传weiboSina源码为例介绍,其下载地址为:http://download.csdn.net/detail/ryzhanglu/3453875

1、项目概况

       该项目文件列表如下:


        其AndroidManifest.xml文件内容为:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
  3.       package="cn.com.hh.view"  
  4.       android:versionCode="1"  
  5.       android:versionName="1.0">  
  6.   
  7.   
  8.     <application android:icon="@drawable/icon" android:label="@string/app_name"  
  9.         android:theme="@android:style/Theme.NoTitleBar.Fullscreen">  
  10. <!--   主Activity    -->  
  11.         <activity android:name="cn.com.hh.view.MyBlogActivity"  
  12.                   android:label="@string/app_name">  
  13.             <intent-filter>  
  14.                 <action android:name="android.intent.action.MAIN" />  
  15.                 <category android:name="android.intent.category.LAUNCHER" />  
  16.             </intent-filter>  
  17.         </activity>  
  18. <!--  注册 授权登录Activity    -->  
  19.         <activity android:name="cn.com.hh.view.AuthorizeActivity" android:launchMode="singleTask">  
  20.             <intent-filter>  
  21.                 <action android:name="android.intent.action.VIEW" />  
  22.                 <category android:name="android.intent.category.DEFAULT" />  
  23.                 <category android:name="android.intent.category.BROWSABLE" />  
  24.                 <data android:scheme="myapp" android:host="AuthorizeActivity" />  
  25.             </intent-filter>  
  26.         </activity>  
  27. <!--  注册登录Activity     -->  
  28.         <activity android:name="cn.com.hh.view.LoginActivity" >             
  29. <!--         <intent-filter>-->  
  30. <!--             <action android:name="android.intent.action.VIEW" />-->  
  31. <!--             <category android:name="android.intent.category.DEFAULT" />-->  
  32. <!--             <category android:name="android.intent.category.BROWSABLE" />-->  
  33. <!--             <data android:scheme="myapp" android:host="AuthorizeActivity" />-->  
  34. <!--         </intent-filter>-->  
  35.         </activity>  
  36. <!--  注册登录Activity     -->  
  37.         <activity android:name="cn.com.hh.view.HomeActivity" >              
  38.         </activity>  
  39. <!--  注册登录Activity     -->  
  40.         <activity android:name="cn.com.hh.view.ViewActivity" >              
  41.         </activity>  
  42.     </application>  
  43.     <uses-permission android:name="android.permission.INTERNET" />  
  44. </manifest>  

2、oauth2认证

       说说关于OAuth授权认证的事情,新浪开放api都必须在这个基础上才能调用,所以有必要专门来讲讲,前面的文章中已经提到过关于新浪微博提供了OAuth和Base OAuth两种认证方式,并且本项目采用OAuth认证方式,至于为什么采用这个OAuth认证而不采用Base OAuth认证原因很简单,自从Twitter只支持OAuth认证方式以来,各大应用都纷纷转向OAuth认证方式,而新浪微博的开放平台也将在近日停止Base OAuth的认证方式。
     OAuth的基本概念,OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。同样新浪微博提供OAuth认证也是为了保证用户账号和密码的安全,在这里通过OAuth建立普通新浪微博用户、客户端程序(我们正在开发的这个Android客户端程序)、新浪微博三者之间的相互信任关系,让客户端程序(我们正在开发的这个android客户端程序)不需要知道用户的账号和密码也能浏览、发布微博,这样有效的保护了用户账号的安全性不需要把账号密码透露给客户端程序又达到了通过客户端程序写微博看微博目的。这个是OAuth的作用。
      结合新浪微博的OAuth认证来说说具体的功能实现,首先罗列一下关键字组,下面四组关键字跟我们接下来OAuth认证有非常大的关系。
      第一组:(App Key和App Secret),这组参数就是本系列文本第一篇提到的建一个新的应用获取App Key和App Secret。
      第二组:(Request Token和Request Secret)
      第三组:(oauth_verifier)
      第四组:(user_id、Access Token和Access Secret) 
     新浪微博的OAuth认证过程,当用户第一次使用本客户端软件时,客户端程序用第一组作为参数向新浪微博发起请求,然后新浪微博经过验证后返回第二组参数给客户端软件同时表示新浪微博信任本客户端软件,当客户端软件获取第二组参数时作为参数引导用户浏览器跳至新浪微博的授权页面,然后用户在新浪的这个授权页面里输入自己的微博账号和密码进行授权,完成授权后根据客户端设定的回调地址把第三组参数返回给客户端软件并表示用户也信任本客户端软件,接下客户端软件把第二组参数和第三组参数作为参数再次向新浪微博发起请求,然后新浪微博返回第四组参数给客户端软件,第四组参数需要好好的保存起来这个就是用来代替用户的新浪账号和密码用的,在后面调用api时都需要。从这个过程来看用户只是在新浪微博的认证网页输入过账户和密码并没有在客户端软件里输入过账户和密码,客户端软件只保存了第四组数据并没有保存用户的账户和密码,这样有效的避免了账户和密码透露给新浪微博之外的第三方应用程序,保证 了安全性。
      本项目用为了方便开发采用了oauth-signpost开源项目进行OAuth认证开发,新建OAuth.java类文件对OA进行简单的封装,OAuth类主要有RequestAccessToken、GetAccessToken、SignRequest三个方法,第一个方法RequestAccessToken就是上面过程中用来获取第三组参数用的,GetAccessToken方法是用来获取第四组参数用,SignRequest方法是用来调用api用。由于采用了oauth-signpost开源项目简单了很多。具体代码如下:

  1. public class OAuth {  
  2.     private CommonsHttpOAuthConsumer httpOauthConsumer;  
  3.     private OAuthProvider httpOauthprovider;  
  4.     public String consumerKey;  
  5.     public String consumerSecret;  
  6.       
  7.     public OAuth()  
  8.     {      
  9.         // 第一组:(App Key和App Secret)  
  10.         // 这组参数就是本系列文本第一篇提到的建一个新的应用获取App Key和App Secret。  
  11.         this("3315495489","e2731e7grf592c0fd7fea32406f86e1b");  
  12.     }  
  13.     public OAuth(String consumerKey,String consumerSecret)  
  14.     {  
  15.         this.consumerKey=consumerKey;  
  16.         this.consumerSecret=consumerSecret;  
  17.     }  
  18.       
  19.     public Boolean RequestAccessToken(Activity activity,String callBackUrl){  
  20.         Boolean ret=false;  
  21.         try{  
  22.             httpOauthConsumer = new CommonsHttpOAuthConsumer(consumerKey,consumerSecret);  
  23.             httpOauthprovider = new DefaultOAuthProvider("http://api.t.sina.com.cn/oauth/request_token","http://api.t.sina.com.cn/oauth/access_token","http://api.t.sina.com.cn/oauth/authorize");  
  24.             String authUrl = httpOauthprovider.retrieveRequestToken(httpOauthConsumer, callBackUrl);  
  25.             activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(authUrl)));  
  26.             ret=true;  
  27.         }catch(Exception e){  
  28.         }  
  29.         return ret;  
  30.     }  
  31.       
  32.     public UserInfo GetAccessToken(Intent intent){  
  33.         UserInfo user=null;  
  34.         Uri uri = intent.getData();  
  35.         String verifier = uri.getQueryParameter(oauth.signpost.OAuth.OAUTH_VERIFIER);  
  36.         try {  
  37.             httpOauthprovider.setOAuth10a(true);   
  38.             httpOauthprovider.retrieveAccessToken(httpOauthConsumer,verifier);  
  39.         } catch (OAuthMessageSignerException ex) {  
  40.             ex.printStackTrace();  
  41.         } catch (OAuthNotAuthorizedException ex) {  
  42.             ex.printStackTrace();  
  43.         } catch (OAuthExpectationFailedException ex) {  
  44.             ex.printStackTrace();  
  45.         } catch (OAuthCommunicationException ex) {  
  46.             ex.printStackTrace();  
  47.         }  
  48.         SortedSet<String> user_id= httpOauthprovider.getResponseParameters().get("user_id");  
  49.         String userId=user_id.first();  
  50.         String userKey = httpOauthConsumer.getToken();  
  51.         String userSecret = httpOauthConsumer.getTokenSecret();  
  52.         user=new UserInfo();  
  53.         user.setUserId(userId);  
  54.         user.setToken(userKey);  
  55.         user.setTokenSecret(userSecret);  
  56.         return user;  
  57.     }  
  58.       
  59.     public HttpResponse SignRequest(String token,String tokenSecret,String url,List params)  
  60.     {  
  61.         HttpPost post = new HttpPost(url);  
  62.         //HttpClient httpClient = null;  
  63.         try{  
  64.             post.setEntity(new UrlEncodedFormEntity(params,HTTP.UTF_8));  
  65.         } catch (UnsupportedEncodingException e) {  
  66.              e.printStackTrace();  
  67.         }  
  68.         //关闭Expect:100-Continue握手  
  69.         //100-Continue握手需谨慎使用,因为遇到不支持HTTP/1.1协议的服务器或者代理时会引起问题  
  70.         post.getParams().setBooleanParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, false);  
  71.         return SignRequest(token,tokenSecret,post);  
  72.     }  
  73.       
  74.     public HttpResponse SignRequest(String token,String tokenSecret,HttpPost post){  
  75.         httpOauthConsumer = new CommonsHttpOAuthConsumer(consumerKey,consumerSecret);  
  76.         httpOauthConsumer.setTokenWithSecret(token,tokenSecret);  
  77.         HttpResponse response = null;  
  78.         try {  
  79.             httpOauthConsumer.sign(post);  
  80.         } catch (OAuthMessageSignerException e) {  
  81.             e.printStackTrace();  
  82.         } catch (OAuthExpectationFailedException e) {  
  83.             e.printStackTrace();  
  84.         } catch (OAuthCommunicationException e) {  
  85.             e.printStackTrace();  
  86.         }  
  87.         //取得HTTP response  
  88.         try {  
  89.             response = new DefaultHttpClient().execute(post);  
  90.         } catch (ClientProtocolException e) {  
  91.             e.printStackTrace();  
  92.         } catch (IOException e) {  
  93.             e.printStackTrace();  
  94.         }  
  95.         return response;  
  96.     }  
  97. }  
2、认证界面

        根据上文的介绍,我们知道,需要获得用户的授权才可以获得对该用户新浪微博的操作权限,在项目中对此是这样处理的。其界面如下:

        用户进入应用以后,会转到是否对新浪微博进行授权界面,点击“开始”后启动浏览器进入新浪微博授权界面,该界面是新浪制作的,与本应用无关。用户输入账号和密码以后,点击“授权”,进行授权操作。该界面源码如下:

  1. public class AuthorizeActivity extends Activity {  
  2.     private Dialog dialog;  
  3.     private OAuth auth;  
  4.     private static final String CallBackUrl = "myapp://AuthorizeActivity";  
  5.       
  6.     @Override  
  7.     protected void onCreate(Bundle savedInstanceState) {  
  8.         super.onCreate(savedInstanceState);  
  9.         setContentView(R.layout.authorize);  
  10.           
  11.           
  12.         View diaView=View.inflate(this, R.layout.dialog, null);  
  13.         dialog = new Dialog(AuthorizeActivity.this,R.style.dialog);  
  14.         dialog.setContentView(diaView);  
  15.         dialog.show();  
  16.           
  17.         ImageButton stratBtn=(ImageButton)diaView.findViewById(R.id.btn_start);  
  18.         stratBtn.setOnClickListener(new OnClickListener(){  
  19.             public void onClick(View arg0) {  
  20.                 auth=new OAuth("30632531","f539cb169860ed99cf8c1861c5da34f6");  
  21.                 auth.RequestAccessToken(AuthorizeActivity.this, CallBackUrl);  
  22.             }  
  23.               
  24.         });  
  25.   
  26.     }  
  27.       
  28.     @Override  
  29.     protected void onNewIntent(Intent intent) {  
  30.             super.onNewIntent(intent);  
  31.             //在这里处理获取返回的oauth_verifier参数  
  32.             UserInfo user= auth.GetAccessToken(intent);  
  33.             if(user!=null){  
  34.                         DataHelper helper=new DataHelper(this);  
  35.                         String uid=user.getUserId();  
  36.                         if(helper.HaveUserInfo(uid))  
  37.                         {  
  38.                             helper.UpdateUserInfo(user);  
  39.                             Log.e("UserInfo""update");  
  40.                         }else  
  41.                         {  
  42.                             helper.SaveUserInfo(user);  
  43.                             Log.e("UserInfo""add");  
  44.                         }  
  45.                    }  
  46.   
  47.     }  
  48.   
  49. }  
        从该源码中我们可以发现,在AuthorizeActivity界面,用户点击“授权”以后,又重新进入AuthorizeActivity界面(这一步与AndroidManifest.xml文件中AuthorizeActivity项的配置有关),此时会调用onNewIntent接口。

       这里有个地方需要注意,授权成功后重新进入AuthorizeActivity界面,执行onNewIntent函数后并不会自动跳转到其他界面。这里需要添加新的跳转代码。

3、onNewIntent调用时机

        这一部分介绍一下onNewIntent的调用时机。

在IntentActivity中重写下列方法:onCreate onStart onRestart  onResume  onPause onStop onDestroy  onNewIntent
3.1、其他应用发Intent,执行下列方法:
I/@@@philn(12410): onCreate
I/@@@philn(12410): onStart
I/@@@philn(12410): onResume
发Intent的方法:
Uri uri = Uri.parse("philn://blog.163.com");
Intent it = new Intent(Intent.ACTION_VIEW, uri);    
startActivity(it);
3.2、接收Intent声明:

  1. <activity android:name="cn.com.hh.view.AuthorizeActivity" android:launchMode="singleTask">  
  2.         <intent-filter>  
  3.             <action android:name="android.intent.action.VIEW" />  
  4.             <category android:name="android.intent.category.DEFAULT" />  
  5.             <category android:name="android.intent.category.BROWSABLE" />  
  6.             <data android:scheme="myapp" android:host="AuthorizeActivity" />  
  7.         </intent-filter>  
  8.        </activity>  
3.3、如果IntentActivity处于任务栈的顶端(AndroidManifest.xml配置中的android:launchMode="singleTask" 项),也就是说之前打开过的Activity,现在处于
I/@@@philn(12410): onPause
I/@@@philn(12410): onStop 状态的话
其他应用再发送Intent的话,执行顺序为:
I/@@@philn(12410): onNewIntent
I/@@@philn(12410): onRestart
I/@@@philn(12410): onStart
I/@@@philn(12410): onResume
        在Android应用程序开发的时候,从一个Activity启动另一个Activity并传递一些数据到新的Activity上非常简单,但是当您需要让后台运行的Activity回到前台并传递一些数据可能就会存在一点点小问题。
        首先,在默认情况下,当您通过Intent启到一个Activity的时候,就算已经存在一个相同的正在运行的Activity,系统都会创建一个新的Activity实例并显示出来。为了不让Activity实例化多次,我们需要通过在AndroidManifest.xml配置activity的加载方式(launchMode)以实现单任务模式,如下所示:
  1. <activity android:label="@string/app_name" android:launchmode="singleTask"android:name="Activity1">  
  2. </activity>  
       launchMode为singleTask的时候,通过Intent启到一个Activity,如果系统已经存在一个实例,系统就会将请求发送到这个实例上,但这个时候,系统就不会再调用通常情况下我们处理请求数据的onCreate方法,而是调用onNewIntent方法,如下所示:
  1. protected void onNewIntent(Intent intent) {  
  2.    super.onNewIntent(intent);  
  3.    setIntent(intent);//must store the new intent unless getIntent() will return the old one  
  4.    processExtraData();  
  5.   
  6.  }  
       不要忘记,系统可能会随时杀掉后台运行的Activity,如果这一切发生,那么系统就会调用onCreate方法,而不调用onNewIntent方法,一个好的解决方法就是在onCreate和onNewIntent方法中调用同一个处理数据的方法,如下所示:
  1. public void onCreate(Bundle savedInstanceState) {  
  2.   super.onCreate(savedInstanceState);  
  3.   setContentView(R.layout.main);  
  4.   processExtraData();  
  5. }  
  6.    
  7. protected void onNewIntent(Intent intent) {  
  8.   super.onNewIntent(intent);  
  9.   setIntent(intent);//must store the new intent unless getIntent() will return the old one  
  10.  processExtraData()  
  11.   
  12. }  
  13.   
  14. private void processExtraData(){  
  15.   Intent intent = getIntent();  
  16.   //use the data received here  
  17. }  

4、获得微博数据

        获得用户认证后,就可以获得第四组参数,就可以依次获得用户数据了。

       在项目中这一步是在HomeActivity界面中完成的,其中获得微博数据部分函数的源码如下:

  1. private void loadList(){  
  2.       if(ConfigHelper.nowUser==null)  
  3.       {  
  4.             
  5.       }  
  6.       else  
  7.       {  
  8.           user=ConfigHelper.nowUser;  
  9.           //显示当前用户名称  
  10.           TextView showName=(TextView)findViewById(R.id.showName);  
  11.           showName.setText(user.getUserName());  
  12.             
  13.           OAuth auth=new OAuth();  
  14.           String url = "http://api.t.sina.com.cn/statuses/friends_timeline.json";  
  15. //          String url = "http://api.t.sina.com.cn/statuses/public_timeline.json";  
  16.           List<BasicNameValuePair> params=new ArrayList<BasicNameValuePair>();  
  17.           params.add(new BasicNameValuePair("source", auth.consumerKey));   
  18.           HttpResponse response = auth.SignRequest(user.getToken(), user.getTokenSecret(), url, params);  
  19.           if (200 == response.getStatusLine().getStatusCode()){  
  20.               try {  
  21.                   InputStream is = response.getEntity().getContent();  
  22.                   Reader reader = new BufferedReader(new InputStreamReader(is), 4000);  
  23.                   StringBuilder buffer = new StringBuilder((int) response.getEntity().getContentLength());  
  24.                   try {  
  25.                       char[] tmp = new char[1024];  
  26.                       int l;  
  27.                       while ((l = reader.read(tmp)) != -1) {  
  28.                           buffer.append(tmp, 0, l);  
  29.                       }  
  30.                   } finally {  
  31.                       reader.close();  
  32.                   }  
  33.                   String string = buffer.toString();  
  34.                   //Log.e("json", "rs:" + string);  
  35.                   ((org.apache.http.HttpResponse) response).getEntity().consumeContent();  
  36.                   JSONArray data=new JSONArray(string);  
  37.                   for(int i=0;i<data.length();i++)  
  38.                   {  
  39.                       JSONObject d=data.getJSONObject(i);  
  40.                       //Log.e("json", "rs:" + d.getString("created_at"));  
  41.                       if(d!=null){  
  42.                           JSONObject u=d.getJSONObject("user");  
  43.                           if(d.has("retweeted_status")){  
  44.                               JSONObject r=d.getJSONObject("retweeted_status");  
  45.                           }  
  46.                             
  47.                           //微博id  
  48.                           String id=d.getString("id");  
  49.                           String userId=u.getString("id");  
  50.                           String userName=u.getString("screen_name");  
  51.                           String userIcon=u.getString("profile_image_url");  
  52. //                          Log.e("userIcon", userIcon);  
  53.                           String time=d.getString("created_at");  
  54.                           String text=d.getString("text");  
  55.                           Boolean haveImg=false;  
  56.                           if(d.has("thumbnail_pic")){  
  57.                               haveImg=true;  
  58.                               //String thumbnail_pic=d.getString("thumbnail_pic");  
  59.                               //Log.e("thumbnail_pic", thumbnail_pic);  
  60.                           }  
  61.                             
  62.                           Date startDate=new Date(time);  
  63.                           Date nowDate = Calendar.getInstance().getTime();  
  64.                           time=new DateUtilsDef().twoDateDistance(startDate,nowDate);  
  65.                           if(wbList==null){  
  66.                               wbList=new ArrayList<WeiBoInfo>();  
  67.                           }  
  68.                           WeiBoInfo w=new WeiBoInfo();  
  69.                           w.setId(id);  
  70.                           w.setUserId(userId);  
  71.                           w.setUserName(userName);  
  72.                           w.setTime(time +" 前");  
  73.                           w.setText(text);  
  74.                             
  75.                           w.setHaveImage(haveImg);  
  76.                           w.setUserIcon(userIcon);  
  77.                           wbList.add(w);  
  78.                       }  
  79.                   }  
  80.                     
  81.               }catch (IllegalStateException e) {  
  82.                   e.printStackTrace();  
  83.               } catch (IOException e) {  
  84.                   e.printStackTrace();  
  85.               } catch (JSONException e) {  
  86.                   e.printStackTrace();  
  87.               }   
  88.           }  
  89.             
  90.           if(wbList!=null)  
  91.           {  
  92.               WeiBoAdapater adapater = new WeiBoAdapater();  
  93.               ListView Msglist=(ListView)findViewById(R.id.Msglist);  
  94.               Msglist.setOnItemClickListener(new OnItemClickListener(){  
  95.                    
  96.                   public void onItemClick(AdapterView<?> arg0, View view,int arg2, long arg3) {  
  97.                       Object obj=view.getTag();  
  98.                       if(obj!=null){  
  99.                           String id=obj.toString();  
  100.                           Intent intent = new Intent(HomeActivity.this,ViewActivity.class);  
  101.                           Bundle b=new Bundle();  
  102.                           b.putString("key", id);  
  103.                           intent.putExtras(b);  
  104.                           startActivity(intent);  
  105.                       }  
  106.                   }  
  107.                     
  108.               });  
  109.               Msglist.setAdapter(adapater);  
  110.           }  
  111.       }  
  112.       loadingLayout.setVisibility(View.GONE);  
  113.   }  
        其中根据新浪提供的API对用户数据进行操作。这里只是获得用户所发布的微博信息,其api为http://api.t.sina.com.cn/statuses/friends_timeline.json。这里同时需要提供第四组参数,params.add(new BasicNameValuePair("source", auth.consumerKey));。其返回的数据格式是json需要对其进行解析。对于json格式的解析可以参看本人博文《android基础知识11:json解析及简单例子》。


参考文献:

http://www.cnblogs.com/hll2008/archive/2011/01/03/1923674.html

及其系列文章
原文地址:https://www.cnblogs.com/miaozhenzhong/p/5931024.html