2010年1月10日日曜日

Android用のTwitterクライアントを作ってみる2

前回のアプリをもうちょっとのリッチに・・・
っていうか、あんなのじゃアプリというにもおこがましいですよねw
というわけで、どう考えても必要な機能を追加してみました。

ID/Pass の設定

前回のソースを軸に ID等を設定するActivityを作成し、Intentを使用して呼び出します。
ID等の保管には SharedPreferences を使用するので、一度設定すればアプリがアンインストールされるまで「たぶん」記憶されます。
以下のような感じで作成します。

  • 本体Activity(Main.java)
    • アプリ起動
    • ID/Passが設定されているかを確認
    • されてなければ設定画面を呼び出す
    • ID設定Activityの呼び出しは、menuボタン押下時のオプションメニューに追加
    • 後は前回と同じような感じ
  • ID設定Activity(SetID.java)
    • ID/Passの現在設定を取得しEditTextに設定
    • OKが押されたら、ID等を認証確認し問題なければ設定
    • Cancelが押されたら、現在設定のまま終了(ただし現在設定すらない場合はNG)

layout/main.xml

前回から変更なし


layout/setid.xml

ボタン二つとEditText二つ備えたlayoutを作成すればよし。
android:inputType="textPassword" をEditTextに指定すれば、いかにもパスワード入力っぽくなります。


AndroidManifest.xml

アクティビティを増やしたので、追加したものはしっかり記述する。
忘れると実行時に怒られます。
・<activity android:name=".SetID"></activity>


value/strings.xml

文字列をlayoutで設定した場合は、このxmlを編集します。


SetID.java
package com.omokageru.ak.twitter;

import java.io.IOException;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;

import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class SetID extends Activity {

 private String orgid;
 private String orgpass;
 private String tempid;
 private String temppass;

 private EditText idEdit;
 private EditText passEdit;
 private Button saveButton;
 private Button cancelButton;

 /* (非 Javadoc)
  * @see android.app.Activity#onCreate(android.os.Bundle)
  */
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  // TODO 自動生成されたメソッド・スタブ
  super.onCreate(savedInstanceState);
  setContentView(R.layout.setid);

  idEdit = (EditText) findViewById(R.id.setid_edit);
  passEdit = (EditText) findViewById(R.id.setpass_edit);
  saveButton = (Button) findViewById(R.id.button_save);
  cancelButton = (Button) findViewById(R.id.button_cancel);

  // プリファレンスからID/Passを取得
  SharedPreferences preferences = getSharedPreferences( getString(R.string.preferences_name), MODE_PRIVATE);
  orgid = preferences.getString( getString(R.string.preferences_id), "");
  orgpass = preferences.getString( getString(R.string.preferences_pass), "");

  // 初期値の設定
  idEdit.setText(orgid);
  passEdit.setText(orgpass);
  tempid = "";
  temppass = "";

  saveButton.setOnClickListener(new OnClickListener() {

   public void onClick(View v) {
    // TODO 自動生成されたメソッド・スタブ
    tempid = idEdit.getEditableText().toString();
    temppass = passEdit.getEditableText().toString();

    // tempid == null とか tempid == "" だと引っかからなかったので
    if( tempid.length() == 0 || temppass.length() == 0){
     Toast.makeText( SetID.this, "ID / pass の設定がありません", Toast.LENGTH_LONG).show();
     return;
    }

    // id/pass が正しいかを確認
    if( !chkid()){
     Toast.makeText( SetID.this, "ID / pass を見直してください", Toast.LENGTH_LONG).show();
     return;
    }

    // 新しい ID/pass を設定して終了
    SharedPreferences preferences = getSharedPreferences( getString(R.string.preferences_name), MODE_PRIVATE);
    SharedPreferences.Editor editor = preferences.edit();
    editor.putString( getString(R.string.preferences_id), tempid);
    editor.putString( getString(R.string.preferences_pass), temppass);
    editor.commit();
    setResult(RESULT_OK);
    finish();

   }
  });

  cancelButton.setOnClickListener(new OnClickListener() {

   public void onClick(View v) {
    // TODO 自動生成されたメソッド・スタブ
    // cancel といえども、まったく設定のない場合は許可しない
    if( orgid.length() == 0 || orgpass.length() == 0){
     Toast.makeText( SetID.this, "ID / pass の設定がありません", Toast.LENGTH_LONG).show();
     return;
    }
    setResult(RESULT_CANCELED);
    finish();
   }
  });
 }

 private boolean chkid(){
  try {
   DefaultHttpClient httpClient = new DefaultHttpClient();
   Credentials cred = new UsernamePasswordCredentials( tempid, temppass);
   httpClient.getCredentialsProvider().setCredentials( new AuthScope("twitter.com", 80), cred);

   HttpGet get = new HttpGet( "http://twitter.com/account/verify_credentials.json");
   HttpResponse response = httpClient.execute(get);

   // 失敗時の処理
   if(response.getStatusLine().getStatusCode() != HttpStatus.SC_OK){
    return false;
   }

   // 一応セッションを閉じておく
   HttpPost post = new HttpPost( "http://twitter.com/account/end_session.json");
   httpClient.execute(post);

   return true;

  } catch (ClientProtocolException e) {
   // TODO 自動生成された catch ブロック
   Toast.makeText( SetID.this, "予期せぬエラー SetID#chkid ClientProtocolException", Toast.LENGTH_LONG).show();
  } catch (IOException e) {
   // TODO 自動生成された catch ブロック
   Toast.makeText( SetID.this, "予期せぬエラー SetID#chkid IOException", Toast.LENGTH_LONG).show();
  }
  return false;
 }


}

なお、ID/Passの認証のチェックには
http://twitter.com/account/verify_credentials.format
を使用。

「この API は認証情報(BASIC認証で使うユーザ名とパスワードの組み合わせ)が有効かどうかを確認するのに利用することができる」
とありましたので、そのようにしました。

一応認証に成功したら
http://twitter.com/account/end_session.format
を呼び出しています。

TwitterAPIの仕様書を日本語に翻訳してくれている方がいらっしゃいますので、詳細はそちらを
観測気球様 Twitter API 仕様書 (勝手に日本語訳シリーズ)


Main.java
/*
 *
 * HTTPステータスコード(レスポンス)
 * 200 OK:                    成功
 * 304 Not Modified:          新しい情報はない
 * 400 Bad Request:           API の実行回数制限に引っ掛かった、などの理由でリクエストを却下した
 * 401 Not Authorized:        認証失敗
 * 403 Forbidden:             権限がないAPI を実行しようとした(following ではない protected なユーザの情報を取得しようとした、など)
 * 404 Not Found:             存在しない API を実行しようとしたり、存在しないユーザを引数で指定して API を実行しようとした
 * 500 Internal Server Error: Twitter 側で何らかの問題が発生している
 * 502 Bad Gateway:           Twitter のサーバが止まっている、あるいはメンテ中
 * 503 Service Unavailable:   Twitter のサーバの負荷が高すぎて、リクエストを裁き切れない状態になっている
 */

package com.omokageru.ak.twitter;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class Main extends Activity {

 private Button updateButton;
 private Button reloadButton;
 private EditText updateEdit;
 private WebView webView;

 private DefaultHttpClient httpClient;
 private String strpass;
 private String strid;


 /* (非 Javadoc)
  * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
  */
 /**
  * オプションメニューの追加
  */
 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // TODO 自動生成されたメソッド・スタブ
  super.onCreateOptionsMenu(menu);

  // 第二引数で設定するItemIDで
  // onMenuItemSelected で判定する
  menu.add( 0, 0, 0,R.string.Set_id_pass);
  return true;
 }

 /* (非 Javadoc)
  * @see android.app.Activity#onMenuItemSelected(int, android.view.MenuItem)
  */
 /**
  * 選択されたオプションメニューの動作
  */
 @Override
 public boolean onMenuItemSelected(int featureId, MenuItem item) {
  // TODO 自動生成されたメソッド・スタブ
  super.onMenuItemSelected(featureId, item);

  switch( item.getItemId()){
  case 0: //R.string.Set_id_pass
   GoSetTwitterIDAndPassActivity();
   break;
  }
  return true;
 }

 /* (非 Javadoc)
  * @see android.app.Activity#onActivityResult(int, int, android.content.Intent)
  */
 /**
  * 子アクティビティからの結果を受け取る
  */
 @Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  // TODO 自動生成されたメソッド・スタブ
//  super.onActivityResult(requestCode, resultCode, data);
  if (resultCode == RESULT_OK) {
   SetTwitterIDAndPass(false);
  }
 }

 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  updateButton = (Button) findViewById(R.id.UpdateButton);

  // ボタン押下時の処理
  updateButton.setOnClickListener(new OnClickListener() {

   public void onClick(View v) {
    // TODO 自動生成されたメソッド・スタブ

    if(!chkid(false)){
     return;
    }

    // 通信を伴うので別スレッドで
    new Thread(){
     @Override
     public void run() {
      // TODO 自動生成されたメソッド・スタブ
      updateButton.setClickable(false);
      reloadButton.setClickable(false);
      String str = updateEdit.getEditableText().toString();
      UpdateTwitter(str);
      reloadButton.setClickable(true);
      updateButton.setClickable(true);
     }
    }.start();
   }
  });

  reloadButton = (Button) findViewById(R.id.ReloadButton);

  reloadButton.setOnClickListener(new OnClickListener() {

   public void onClick(View view) {
    // TODO 自動生成されたメソッド・スタブ

    if(!chkid(false)){
     return;
    }

    // 通信を伴うので別スレッドで
    new Thread(){
     @Override
     public void run() {
      updateButton.setClickable(false);
      reloadButton.setClickable(false);
      GetAndWriteHomeTimeLine();
      reloadButton.setClickable(true);
      updateButton.setClickable(true);
     }
    }.start();
   }
  });

  updateEdit = (EditText) findViewById(R.id.UpdateEdit);
  webView = (WebView) findViewById(R.id.twitter_web);

  SetTwitterIDAndPass(true);
    }

 /**
  * Twitterへつぶやく
  * @param str
  * 投稿内容
  */
 private synchronized void UpdateTwitter( String str){
  try {
   // パラメータstatus=hoge でhogeを発言(必須)
   HttpPost post = new HttpPost( "http://twitter.com/statuses/update.json?status=" + str);
   // 一応これで発言するはず
   HttpResponse response = httpClient.execute(post);

   // 失敗時の処理
   if(response.getStatusLine().getStatusCode() != HttpStatus.SC_OK){
    Toast.makeText( Main.this, "API制限か、Twitter ID/Password が間違っている可能性があります", Toast.LENGTH_LONG).show();
    return;
   }

   // ここからの描画はなぜか利かない・・・
   GetAndWriteHomeTimeLine();

  } catch (ClientProtocolException e) {
   // TODO 自動生成された catch ブロック
   Toast.makeText( Main.this, "予期せぬエラー Main#UpdateTwitter ClientProtocolException", Toast.LENGTH_LONG).show();
  } catch (IOException e) {
   // TODO 自動生成された catch ブロック
   Toast.makeText( Main.this, "予期せぬエラー Main#UpdateTwitter IOException", Toast.LENGTH_LONG).show();
  }

 }

 /**
  * friend_timelineは将来廃止予定らしいので home_timeline を取得し表示。
* http://d.hatena.ne.jp/nowokay/20091030 を参考にしました。 */ private synchronized void GetAndWriteHomeTimeLine(){ try { // webView.clearCache(false); HttpGet get = new HttpGet( "http://twitter.com/statuses/home_timeline.json"); // これで取得するはず HttpResponse response = httpClient.execute(get); // 失敗時の処理 if(response.getStatusLine().getStatusCode() != HttpStatus.SC_OK){ Toast.makeText( Main.this, "API制限か、Twitter ID/Password が間違っている可能性があります", Toast.LENGTH_LONG).show(); return; } //解析と出力 //サーバーからのデータを取得 InputStream is = response.getEntity().getContent(); InputStreamReader isr = new InputStreamReader(is); StringWriter strin = new StringWriter(); BufferedReader buf = new BufferedReader(isr); for(String line; (line = buf.readLine()) != null;){ strin.write(line); } buf.close(); isr.close(); is.close(); //出力準備 StringWriter strout = new StringWriter(); PrintWriter out = new PrintWriter(strout); out.println("<html><head><title>Twitter testapites</title></head>"); out.println("<body>"); //JSONデータからタイムラインを取得してHTMLを生成 JSONTokener token = new JSONTokener(strin.toString()); JSONArray arr = new JSONArray(token); for(int i = 0; i < arr.length(); i++){ JSONObject obj = arr.getJSONObject(i); JSONObject user = obj.getJSONObject("user"); out.println("
"); out.println(""); out.println("" + user.getString("screen_name") + "
" ); out.println(obj.get("text") + "
"); // ここで出力を確認しているが、発言内容が含まれているにもかかわらず、 // update側からはなぜか更新されない。 Log.i("t_test", "" + obj.get("text")); out.println("
"); } out.println("</body></html>"); strin.close(); out.close(); // 二回呼び出すと反映される・・・ // invalidate() の意味は?? webView.loadData(strout.toString(), "text/html", "utf-8"); webView.loadData(strout.toString(), "text/html", "utf-8"); strout.close(); // 描画更新 // 本体スレッドからの呼び出しではない場合に備えpostInvalidateを使用。 // webView.postInvalidate(); // Log.i("t_test", "Invalidate"); } catch (ClientProtocolException e) { // TODO 自動生成された catch ブロック Toast.makeText( Main.this, "予期せぬエラー Main#GetAndWriteHomeTimeLine ClientProtocolException", Toast.LENGTH_LONG).show(); } catch (JSONException e) { // TODO 自動生成された catch ブロック Toast.makeText( Main.this, "予期せぬエラー Main#GetAndWriteHomeTimeLine JSONException", Toast.LENGTH_LONG).show(); } catch (IOException e) { // TODO 自動生成された catch ブロック Toast.makeText( Main.this, "予期せぬエラー Main#GetAndWriteHomeTimeLine IOException", Toast.LENGTH_LONG).show(); } } /** * TwitterのIDとPassを設定する * @param bCreate * 初回起動時はtrueを指定 */ private void SetTwitterIDAndPass( boolean bCreate){ updateButton.setClickable(false); reloadButton.setClickable(false); // プリファレンスからID/Passを取得 SharedPreferences preferences = getSharedPreferences( getString(R.string.preferences_name), MODE_PRIVATE); strid = preferences.getString( getString(R.string.preferences_id), ""); strpass = preferences.getString( getString(R.string.preferences_pass), ""); if(!chkid( bCreate)){ return; } httpClient = new DefaultHttpClient(); Credentials cred = new UsernamePasswordCredentials( strid, strpass); httpClient.getCredentialsProvider().setCredentials( new AuthScope("twitter.com", 80), cred); GetAndWriteHomeTimeLine(); updateButton.setClickable(true); reloadButton.setClickable(true); } /** * Twitter ID/pass が設定されているかを確認 * @param bCreate * 初回起動時はtrueを指定 * @return * boolean値 */ private boolean chkid( boolean bCreate){ if(strid.length() == 0 || strpass.length() == 0){ // 初回起動時にid/passが未設定なら、設定画面を呼び出す if(bCreate){ GoSetTwitterIDAndPassActivity(); } return false; } return true; } /** * Twitter ID/pass を設定するActivityへ移動 */ private void GoSetTwitterIDAndPassActivity(){ try{ Intent intent = new Intent(Main.this, SetID.class); startActivityForResult(intent, 0); } catch( ActivityNotFoundException e){ Toast.makeText( Main.this, "起動先のActivityが見つかりません", Toast.LENGTH_LONG).show(); } } }

前回からちょこちょこっと変更した箇所もありますが、大きくは

  • Intentを使って別のActivityを呼び出す
  • menuボタンを押した時に、オプションメニューが出る
くらいでしょう。

相変わらず、Activityのサイクルを考慮したコードなんか書いていません。
あげく、TwitterAPIの使用回数制限とかも無視しまくり。
流用しよう、なんていう奇特な方が勝手に直してくれることを期待しています。
ってか、いいサンプルあったり、素敵な修正がある場合は教えてくださいw

次は

  • 何分かに一回 home_timeline を取得に行く
  • 変更があったら通知バーに通知する
というようなサービスでも作りますか?

動かすとこんな感じ。

なんかメニュー画面が出るとか、それっぽい感じがします。

初回起動時、IDとpassを設定


とりあえず入力


認証できなかった場合


認証成功すると、前回作成したような画面が


menuボタン押下時
急にそれっぽく見えるw


menuボタンからID設定画面へ
前回設定したidとpasswordが。(もちろんpassword とかは適当なものに打ち直していますよw)


android:inputType="textPassword" を設定している場合。
なんだかそれっぽいw



参考 「Android プログラミング入門」
  • 第二部 3.2.2 サブアクティビティからの応答を受け付ける
  • 第二部 5.1 プリファレンス
  • 第三部 1.3.5 メニュー

0 件のコメント:

コメントを投稿