Software/Android/アプリケーション開発テキスト
この章では簡易RSSリーダーを開発し、HTTP通信を使用するアプリケーションについて解説します。
このアプリケーションは設定したRSSを取得して、一覧と詳細表示を行います。詳細画面には外部リンクを併せて表示し、タップすることで外部ブラウザが開くようにします。
新規Androidアプリケーションプロジェクトを以下の設定で作成します。
RSSReaderでは3つのアクティビティを使用します。メイン画面であり、一覧表示を行うアクティビティ(図4-1)、一覧からのタップで詳細表示を行うアクティビティ(図4-2)、取得するRSSフィードを設定するアクティビティ(図4-3)の3つです。
各レイアウトを作成する前に、文字列リソースを定義しておきます。strings.xmlに以下を追加して下さい。helloは不要なので削除します。
<string name="get_feed">取得</string> <string name="config">設定</string> <string name="regist">登録</string> <string name="cancel">取消</string> <string name="back">戻る</string> <string name="label_number_of_get">取得件数:</string> <string name="loading">読み込み中...</string> <string-array name="number_of_get"> <item>1件</item> <item>3件</item> <item>5件</item> </string-array>
string-arrayは文字列の配列で、ドロップダウンリスト(Spinner)で使用します。
リスト4-1がメイン/一覧画面のレイアウトファイルになります。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/linearLayout1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dip" > <Button android:id="@+id/Get" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_weight="1" android:text="@string/get_feed" /> <Button android:id="@+id/Config" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_weight="1" android:text="@string/config" /> </LinearLayout> <ListView android:id="@+id/android:list" android:layout_width="fill_parent" android:layout_height="wrap_content" android:fastScrollEnabled="true" > </ListView> </LinearLayout>
横方向のレイアウトにフィード取得ボタンと設定ボタンが並び、その下にリストビューが配置されています。リストビューの名前が「android:list」となっていますが、これは後ほど解説するListActivityでの埋め込みリストビューの名前で、必ずこの名前を使用する必要があります。
次に一覧表示で使用するリストアイテムのレイアウトを作成します。リスト4-2がフィードのタイトルを表示するレイアウトで、グレーの背景に黒のTextViewを置いています。リスト4-3は記事タイトルと出版日を表示するレイアウトで、黒地に白のTextViewを二つ置いています。図4-1でこれらがどのように表示されるか、もう一度確認してみて下さい。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#cccccc" android:orientation="vertical" > <TextView android:id="@+id/FeedTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="FeedTitle" android:textColor="#000000" android:textSize="16dip" android:textStyle="bold" /> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:id="@+id/ArticleTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="ArticleTitle" /> <TextView android:id="@+id/PubDate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:layout_margin="5dip" android:text="pubDate" /> </LinearLayout>
以上でメイン画面のレイアウト作成は完了です。
続いて詳細表示画面のリストを示します(リスト4-4)。
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/ScrollView" android:layout_width="fill_parent" android:layout_height="fill_parent" > <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <Button android:id="@+id/Back" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="@string/back" /> <TextView android:id="@+id/FeedTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:text="FeedTitle" android:textSize="16dip" android:textStyle="bold" /> <TextView android:id="@+id/ArticleTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:text="ArticleTitle" /> <TextView android:id="@+id/PubDate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:layout_marginRight="10dip" android:text="pubDate" /> <android.webkit.WebView android:id="@+id/Description" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_margin="5dip" android:layout_weight="1" > </android.webkit.WebView> </LinearLayout> </ScrollView>
このレイアウトではまず、最上位のレイアウトがLinearLayoutではなくScrollViewになっていることに注目してください。これにより、情報量の多いフィードであってもスクロールして表示することが出来るようになります。
ScrollViewの下に縦方向のLinearLayoutが配置され、前画面に戻るボタン、フィードのタイトル、記事タイトル、出版日を表示するTextView、そしてWebViewが配置されています。
WebViewはandroid.webkitに含まれる埋め込みHTMLブラウザ用のビューで、RSSフィードのdescriptionにはHTMLタグが使用されることが多いためTextViewの代わりに使用しています。
最後に設定画面のレイアウトを作成します(リスト4-5)。
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/ScrollView" android:layout_width="fill_parent" android:layout_height="fill_parent" > <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/linearLayout1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dip" > <TextView android:id="@+id/LabelNumOfGet" android:layout_width="wrap_content" android:layout_height="fill_parent" android:gravity="center_vertical" android:text="@string/label_number_of_get" /> <Spinner android:id="@+id/NumOfGet" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <EditText android:id="@+id/FeedURL01" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" > <requestFocus /> </EditText> <EditText android:id="@+id/FeedURL02" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL03" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL04" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL05" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL06" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL07" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL08" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL09" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL10" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <LinearLayout android:id="@+id/linearLayout2" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dip" > <Button android:id="@+id/Cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_weight="1" android:text="@string/cancel" /> <Button android:id="@+id/Regist" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_weight="1" android:text="@string/regist" /> </LinearLayout> </LinearLayout> </ScrollView>
設定画面もScrollViewを最上位に置き、スクロール可能にしています。上部にはフィード毎の記事取得件数を指定するドロップダウンリスト(Spinner)を配置し、フィードURL入力用EditTextを10個、設定の取消、保存ボタンを続けて配置します。
これで全ての画面レイアウトが完成しました。
各アクティビティを作成する前に、ApplicationクラスとRSSフィード用データクラスを実装していきます。
Applicationクラスはアプリケーションが起動されると単一のインスタンスが生成され、アプリケーション全体でデータを共有出来ます。ここではApplicationクラスを継承していくつかの定数と設定ファイルへのアクセスを提供します。
android.app.ApplicationをベースクラスとしてRSSReaderApplicationを生成し、いくつかの定数とメンバ変数を追加します(リスト4-6)。
public class RSSReaderApplication extends Application { private static final String CONF_NAME = "rssreader.conf"; public static final int DEFAULT_NUM_OF_GET = 3; public static final int NUM_OF_FEED = 10; private int mNumOfGet = DEFAULT_NUM_OF_GET; private String mFeedURL[];
CONF_NAMEはこのアプリケーションで使用する設定ファイル名ですが、設定へのアクセスは全てこのクラスを通して行うためprivateにします。
DEFAULT_NUM_OF_GETはフィード毎の記事取得デフォルト件数、NUM_OF_FEEDはフィードの登録可能数を表す定数値です。
mNumOfGetとmFeedURLは現在の設定値を保存するメンバ変数で、フィード毎の取得件数と取得フィードのURL配列になっています。設定と取得にはアクセサを使用するため、privateにしてあります。
@Override public void onCreate() { mFeedURL = new String[NUM_OF_FEED]; loadConfig(); }
onCreateメソッド(リスト4-7)ではフィードURL配列mFeedURLをNUM_OF_FEED分確保し、後ほど実装するloadConfigメソッドで設定ファイルの読み込みを行なっています。
public int numberOfGet() { return mNumOfGet; } public void setNumberOfGet(int numOfGet) { mNumOfGet = numOfGet; } public String feedURL(int index) { if ((index < 0) || (10 <= index)) { return null; } return mFeedURL[index]; } public void setFeedURL(int index, String url) { if ((index < 0) || (NUM_OF_FEED <= index)) { return; } mFeedURL[index] = url; } public void clearFeedURL() { for (int i = 0; i < NUM_OF_FEED; ++i) { mFeedURL[i] = ""; } }
onCreateに続いて、フィード取得件数と取得フィードURLのアクセサを実装します(リスト4-8)。
public void loadConfig() { SharedPreferences pref = getSharedPreferences(CONF_NAME, MODE_PRIVATE); mNumOfGet = pref.getInt("NumOfGet", DEFAULT_NUM_OF_GET); for (int i = 0; i < NUM_OF_FEED; ++i) { String key = String.format("FeedURL%02d", (i + 1)); mFeedURL[i] = pref.getString(key, ""); } } public void saveConfig() { SharedPreferences pref = getSharedPreferences(CONF_NAME, MODE_PRIVATE); Editor pe = pref.edit(); pe.putInt("NumOfGet", mNumOfGet); for (int i = 0; i < NUM_OF_FEED; ++i) { String key = String.format("FeedURL%02d", (i + 1)); pe.putString(key, mFeedURL[i]); } pe.commit(); }
loadConfig、saveConfigで設定ファイルの読み書きを行います(リスト4-9)。loadConfigのgetSharedPreferencesでSharedPreferencesクラスのインスタンスを取得していますが、指定のファイル(CONF_NAME)が存在しない場合はこの段階で自動的に作成されます。第二引数に渡しているMODE_PRIVATEはこの設定ファイルが本アプリケーションのみでアクセス可能であることを示し、ファイルが自動生成される際のパーミッションにも反映されます。
プリファレンスクラスを利用すると、各設定内容をキーにより呼び出してアクセスすることが出来ます。ここではgetIntとgetStringメソッドでそれぞれフィード取得件数と取得フィードURLを取得しています。
プリファレンスクラス自体には値の書き換えメソッドは用意されていないため、saveConfigではSharedPreferencesを取得したあと、Editorクラスを利用して設定内容を書き換えを行っています。
続いてフィードデータ用のクラスを実装していきます。フィードデータ用クラスは個々のフィードデータを格納するFeedItemと、それをリスト管理するFeedListの二つのクラスで構成されます。また、FeedListではHTTP通信でRSSフィードを取得し、そこからFeedItemを作成する処理も組み込んでいきます。
FeedItemクラスの全リストが4-10になります。RSSフィード(channel)のタイトルと記事双方をこのクラスで取り扱うため、どちらのデータであるかを示すアイテムタイプと、フィードのタイトル、記事のタイトルと出版日、記事詳細と記事のリンクURLをメンバ変数に持ちます。
package com.beatcraft.rssreader; public class FeedItem { public static final int ITEMTYPE_FEEDCHANNEL = 0; public static final int ITEMTYPE_FEEDITEM = 1; private int mItemType; private String mFeedTitle = ""; private String mArticleTitle = ""; private String mPubDate = ""; private String mDescription = ""; private String mLink = ""; public FeedItem(int itemType) { mItemType = itemType; } public int itemType() { return mItemType; } public String feedTitle() { return mFeedTitle; } public void setFeedTitle(String title) { mFeedTitle = title; } public String articleTitle() { return mArticleTitle; } public void setArticleTitle(String title) { mArticleTitle = title; } public String pubDate() { return mPubDate; } public void setPubDate(String pubDate) { mPubDate = pubDate; } public String description() { return mDescription; } public void setDescription(String description) { mDescription = description; } public String link() { return mLink; } public void setLink(String link) { mLink = link; } }
アイテムタイプがITEMTYPE_FEEDCHANNELであればフィードのタイトルデータ、ITEMTYPE_FEEDITEMであれば記事データとなります。
FeedListクラスでは配列リストクラスArrayListにFeedItemを格納して管理します。
public class FeedList { private ArrayList<FeedItem> mList = null; public FeedList() { mList = new ArrayList<FeedItem>(); } public ArrayList<FeedItem> getList() { return mList; } public int count() { if (mList != null) { return mList.size(); } return 0; }
FeedItemをテンプレートとしたArrayListのメンバ変数mListをコンストラクタで生成し、getList、countを追加して外部からのアクセスが行えるようにします(リスト4-11)。
ここまででHTTP通信によるデータの受け皿が準備出来ました。これから実装するgetメソッドで、実際にHTTP通信を行いRSSフィードの内容を取得します(リスト4-12)。AndroidでのHTTP通信にはいくつかの手段が提供されていますが、本稿ではApacheのHTTPクラスライブラリを使用します。
public int get(RSSReaderApplication app) { int success = 0; for (int i = 0; i < RSSReaderApplication.NUM_OF_FEED; ++i) { String url = app.feedURL(i); if (url.equals("") == true) { continue; } DefaultHttpClient client = new DefaultHttpClient(); if (client != null) { client.getParams().setParameter("http.socket.timeout", new Integer(15000)); HttpGet method = null; try { method = new HttpGet(url); } catch (Exception e) { e.printStackTrace(); } if (method == null) { continue; } HttpResponse response = null; try { response = client.execute(method); int ret = response.getStatusLine().getStatusCode(); if (ret == HttpStatus.SC_OK) { InputStream is = response.getEntity().getContent(); if (parse(is, app.numberOfGet()) > 0) { success++; } is.close(); } } catch (Exception e) { e.printStackTrace(); } finally { client.getConnectionManager().shutdown(); } } } return success; }
getメソッドは、引数としてRSSReaderApplicationのインスタンスを受け取ります。これを利用して取得フィードURLを一つずつ取得し(app.feedURLメソッド)、HTTP-GETメソッドによりRSSフィードを取得していきます。
まずはDefaultHttpClientのインスタンスを生成します。生成に成功したら、HTTPクライアントのgetParamsで取得したパラメータリストに対しsetParameterを呼び出し、15秒のタイムアウト値を設定しています。
次にHTTP通信で使用するメソッドクラスのインスタンスを生成します。今回はGETメソッドを使用しますので、HttpGetクラスに取得フィードのURLを渡して生成しています。
メソッドの生成に成功したら、HTTPクライアントのexecuteにこれを渡して呼び出します。executeからはHttpResponseクラスのインスタンスが返ってきますが、これを利用してステータスコードやGET結果を取得することが出来ます。
まずはgetStatusLine().getStatusCodeでステータスコードを取得、これがHttpStatus.SC_OKであればgetEntity().getContentでGET結果の内容を取得しています。GET結果はInputStreamとして返ってきますので、後ほど実装するparseメソッドでこの解析を行います。parseメソッドにはInputStreamの他、フィード取得件数をアプリケーションクラスから取得して引き渡しています。
最後にfinallyブロックでHTTPクライアントからコネクションマネージャーを取得し、shutdownメソッドでリソースの解放を行なっています。
RSSフィードはXMLで記述されているため、これを解析する必要があります。parseメソッド(4-13)ではXmlPullParserを使用してこの解析を行っています。XmlPullParserはXMLPULL API V1に基づいたAndroidでの実装になります。
private int parse(InputStream is, int max) { int count = 0; boolean inChannel = false; boolean inItem = false; FeedItem item = null; String feedTitle = ""; XmlPullParser p = Xml.newPullParser(); try { p.setInput(is, null); int event = p.getEventType(); while (event != XmlPullParser.END_DOCUMENT) { String elem = null; String tmp = null; switch (event) { case XmlPullParser.START_TAG: elem = p.getName(); if (elem.equals("channel") == true) { inChannel = true; item = new FeedItem(FeedItem.ITEMTYPE_FEEDCHANNEL); } else if (elem.equals("item") == true) { if (inChannel == true) { if (item != null) { mList.add(item); item = null; count++; } inChannel = false; } inItem = true; item = new FeedItem(FeedItem.ITEMTYPE_FEEDITEM); item.setFeedTitle(feedTitle); } else if (elem.equals("title") == true) { tmp = p.nextText(); if ((tmp != null) && (item != null)) { if (inChannel == true) { feedTitle = tmp; item.setFeedTitle(tmp); } else if (inItem == true) { item.setArticleTitle(tmp); } } } else if ((elem.equals("pubDate") == true) || (elem.equals("date") == true)) { if (inItem == true) { tmp = p.nextText(); if ((tmp != null) && (item != null)) { item.setPubDate(tmp); } } } else if (elem.equals("description") == true) { if (inItem == true) { tmp = p.nextText(); if ((tmp != null) && (item != null)) { item.setDescription(tmp); } } } else if (elem.equals("link") == true) { if (inItem == true) { tmp = p.nextText(); if ((tmp != null) && (item != null)) { item.setLink(tmp); } } } break; case XmlPullParser.END_TAG: elem = p.getName(); if (elem.equals("channel") == true) { if (inChannel == true) { if (item != null) { mList.add(item); item = null; count++; } inChannel = false; } } else if (elem.equals("item") == true) { if (inItem == true) { if (item != null) { mList.add(item); item = null; count++; max--; if (max == 0) { return count; } } inItem = false; } } } event = p.next(); } } catch (Exception e) { e.printStackTrace(); return 0; } return count; } }
まずXml.newPullParserメソッドでXmlPullParserのインスタンスを生成し、渡されたInputStreamを入力としてセットしています。これにより、取得したRSSフィードの解析が可能になります。XmlPullParserでは、XMLを順次読み取って解析を行います。パーサーのgetEventTypeメソッドで現在のXMLドキュメントの読み取り状態を取得し、これがドキュメントの終了を示すXmlPullParser.END_DOCUMENTになるまでwhileループで解析を続けています。
状態が要素の開始を示すXmlPullParser.START_TAGになったら、パーサーのgetNameメソッドで要素名を取得します。要素名が"channel"であればFeedItemをITEMTYPE_FEEDCHANNELで生成し、その後"title"要素が現れた際にフィードタイトルとして設定しています。
要素名が"item"の場合、フィードタイトルの解析中であればこれをFeedListに追加し、新たにFeedItemをITEMTYPE_FEEDITEMで生成しています。その後"title"が現れた際に記事タイトルを、"pubDate"もしくは"date"が現れたら出版日を、"description"、"link"が現れたら記事詳細とリンクURLをそれぞれ設定します。
状態がXmlPullParser.END_TAGになったら、解析中のFeedItemがあればこれをリストに追加します。ITEMTYPE_FEEDITEMをリストに追加する際には変数countをインクリメントし、その後引数で渡された取得件数以上であれば解析を終了します。
最後にパーサーのnextメソッドを呼び出し、状態を次に進めてwhileループの先頭に戻ります。
以上でRSSフィードの解析は完了です。
取得するフィードを設定するため、設定画面アクティビティを実装します。android.app.ActivityをベースクラスとしてConfigActivityを生成し、いくつかの定数とメンバ変数を追加します(リスト4-14)。ボタンのクリック処理をこのクラスで行うため、View.OnClickListenerのimplementsも追加して下さい。
public class ConfigActivity extends Activity implements View.OnClickListener { private static final int NUM_OF_GET_LIST[] = {1, 3, 5}; private Spinner mNumOfGet; private EditText mFeedURL[];
NUM_OF_GET_LISTはフィード取得数として選択可能な数を格納する定数配列で、ドロップダウンリスト(Spinner)の選択内容に対応した値1/3/5が設定されています。
mNumOfGetとmFeedURLはフィード取得数Spinnerと取得フィールドURLのEditTextウィジェットのインスタンスを保持しておくメンバ変数です。
/** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); this.getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); setContentView(R.layout.config); RSSReaderApplication app = (RSSReaderApplication) getApplication(); ArrayAdapter<CharSequence> adapter; adapter = ArrayAdapter.createFromResource(this, R.array.number_of_get, android.R.layout.simple_spinner_item); adapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); mNumOfGet = (Spinner) findViewById(R.id.NumOfGet); mNumOfGet.setAdapter(adapter); for (int i = 0; i < 3; ++i) { if (NUM_OF_GET_LIST[i] == app.numberOfGet()) { mNumOfGet.setSelection(i); break; } } mFeedURL = new EditText[RSSReaderApplication.NUM_OF_FEED]; mFeedURL[0] = (EditText) findViewById(R.id.FeedURL01); mFeedURL[1] = (EditText) findViewById(R.id.FeedURL02); mFeedURL[2] = (EditText) findViewById(R.id.FeedURL03); mFeedURL[3] = (EditText) findViewById(R.id.FeedURL04); mFeedURL[4] = (EditText) findViewById(R.id.FeedURL05); mFeedURL[5] = (EditText) findViewById(R.id.FeedURL06); mFeedURL[6] = (EditText) findViewById(R.id.FeedURL07); mFeedURL[7] = (EditText) findViewById(R.id.FeedURL08); mFeedURL[8] = (EditText) findViewById(R.id.FeedURL09); mFeedURL[9] = (EditText) findViewById(R.id.FeedURL10); for (int i = 0; i < RSSReaderApplication.NUM_OF_FEED; ++i) { mFeedURL[i].setText(app.feedURL(i)); } Button button; button = (Button) findViewById(R.id.Cancel); button.setOnClickListener(this); button = (Button) findViewById(R.id.Regist); button.setOnClickListener(this); }
onCreateメソッド(リスト4-15)では、requestWindowFeatureでタイトルを非表示にした後、getWindowメソッドで取得したこのアクティビティのトップレベルウィンドウのインスタンスに対しsetSoftInputModeメソッドを呼び出しています。レイアウトには最初に作成したレイアウトのうち、configレイアウトを設定します。
通常、テキストボックス(EditText)を持つアクティビティが表示された際、フォーカスがテキストボックスに設定されるとソフトウェアキーボードが自動的に表示されてしまいます。setSoftInputModeにLayoutParam.SOFT_INPUT_STATE_ALWAYS_HIDDENを渡して呼び出すことで、この挙動を抑制出来ます。
ここで現在の設定を設定画面に反映させるため、RSSReaderApplicationのインスタンスを取得しています。
続いてmNumOfGetに格納したSpinnerに、文字列配列リソースnumber_of_getをセットしたArrayAdapterを設定し、アプリケーションクラスを通じて取得したフィード取得数に対応するインデックスを現在の選択値にします。この際、フィード取得数に対応するSpinnerのインデックスを割り出すため、先ほど追加した定数配列NUM_OF_GET_LISTを参照しています。
取得フィードURLのEditTextを配列mFeedURLに順次保持したあと、アプリケーションクラスを通じて取得したURLを各EditTextに設定します。
最後に取消ボタン、登録ボタンのインスタンスを取得して、このクラスでクリックを処理するようOnClickListenerに設定しています。これにより、各ボタンをクリックするとonClickメソッド(リスト4-16)が呼び出されます。
@Override public void onClick(View view) { int id = view.getId(); switch (id) { case R.id.Cancel: finish(); break; case R.id.Regist: regist(); break; } }
onClickメソッドでは引数で渡されたViewのIDから、取消(Cancel)ボタンであればfinishを呼び出してアクティビティを終了し、登録(Regist)ボタンであれば次に実装するregistメソッドを呼び出しています。
リスト4-17がregistメソッドになります。
private void regist() { RSSReaderApplication app = (RSSReaderApplication) getApplication(); app.setNumberOfGet(NUM_OF_GET_LIST[mNumOfGet.getSelectedItemPosition()]); app.clearFeedURL(); for (int i = 0, j = 0; i < RSSReaderApplication.NUM_OF_FEED; ++i) { String url = mFeedURL[i].getText().toString().trim(); if (url.equals("") == false) { app.setFeedURL(j++, url); } } app.saveConfig(); finish(); }
以上で設定画面アクティビティの実装は完了です。
このアプリケーションのメイン画面であり、RSSフィードを一覧するアクティビティを実装していきます。
今回はアイテムタイプによってリストビューの各行に設定するレイアウトを切り替えるため、独自のArrayAdapterを作る必要があります。 ベースクラスをArrayAdapter、テンプレートをFeedItemとしてFeedAdapterクラスを生成し、メンバ変数とコンストラクタを実装します(リスト4-18)。
public class FeedAdapter extends ArrayAdapter<FeedItem> { private LayoutInflater mInflate; public FeedAdapter(Context context, List<FeedItem> obj) { super(context, 0, obj); mInflate = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE); }
メンバ変数にLayoutInflaterクラスを追加し、コンストラクタでインスタンスを取得しています。LayoutInflaterはレイアウトXMLを読み込んで動的にViewを生成するクラスです。今回作成するアダプターではフィードのアイテムタイプ(フィードタイトルか記事か)によって使用するレイアウトを切り替えるため、LayoutInflaterを使用します。
続いてisEnabledメソッドを追加します(リスト4-19)。
@Override public boolean isEnabled(int pos) { FeedItem item = getItem(pos); if (item.itemType() == FeedItem.ITEMTYPE_FEEDCHANNEL) { return false; } return true; }
isEnabledメソッドはリストビューの各行に対して呼び出されるメソッドで、falseを返すとその行は無効扱いとなり、行タップなどに反応しなくなります。フィードタイトル行では詳細画面を開くことがなく、無効扱いとするためアイテムタイプがFeedItem.ITEMTYPE_FEEDCHANNELの場合にfalseを返しています。
public View getView(final int pos, View convView, ViewGroup parent) { View view = convView; FeedItem item = getItem(pos); switch (item.itemType()) { case FeedItem.ITEMTYPE_FEEDCHANNEL: view = buildChannel(item); break; case FeedItem.ITEMTYPE_FEEDITEM: view = buildItem(item); break; } return view; }
getViewメソッド(リスト4-20)はリストの各行がスクロールなどによって表示された際に呼び出されます。引数に渡されたposでgetItemを呼び出すと、アダプタに登録されたリストから該当位置のデータを取得出来ます。これがITEMTYPE_FEEDCHANNELであればフィードタイトル用のViewをbuildChannel呼び出しにより生成、ITEMTYPE_FEEDITEMであれば記事用のViewをbuildItem呼び出しにより生成して返します。
リスト4-21がbuildChannel/buildItemメソッドになります。
private View buildChannel(FeedItem item) { View view = null; view = mInflate.inflate(R.layout.item_channel, null); TextView tv; tv = (TextView) view.findViewById(R.id.FeedTitle); tv.setText(item.feedTitle()); return view; } private View buildItem(FeedItem item) { View view = null; view = mInflate.inflate(R.layout.item_item, null); TextView tv; tv = (TextView) view.findViewById(R.id.ArticleTitle); tv.setText(item.articleTitle()); tv = (TextView) view.findViewById(R.id.PubDate); tv.setText(item.pubDate()); return view; }
それぞれitem_channelレイアウト、item_itemレイアウトを使用してLayoutInflaterによりViewを生成したあと、FeedItemからタイトルや出版日、詳細などを設定しています。
メイン画面ではFeedListを介してHTTP通信を行うことになりますが、これをメインスレッドで行うとANRが発生する可能性があります。これを避けるため、バックグラウンドで行うためのAsyncTaskクラスを作成し使用します。
まずはAsyncTaskから呼び出すメソッドのインターフェースを作成します。Package ExplorerでRSSReaderのsrc/com.beatcraft.rssreaderを右クリックし、メニューから「File > Interface」を選択して下さい。"New Java Interface"ダイアログが表示されたら、Nameに「ITaskEntity」と入力して「Finish」ボタンをクリックします。
生成されたITaskEntityにbackgroundProcとpostProcという二つのメソッドインターフェースを宣言します(リスト4-22)。
package com.beatcraft.rssreader; public interface ITaskEntity { void backgroundProc(); void postProc(); }
続いてAsyncTaskを作成します。ベースクラスをAsyncTask、テンプレートをITaskEntity, Integer, VoidとしてHttpAccessTaskを生成し、必要なメソッドを実装していきます(リスト4-23)。
package com.beatcraft.rssreader; import android.app.Activity; import android.app.ProgressDialog; import android.os.AsyncTask; public class HttpAccessTask extends AsyncTask<ITaskEntity, Integer, Void> { private Activity mActivity; private ProgressDialog mDialog; private ITaskEntity mITaskEntity; public HttpAccessTask(Activity activity) { mActivity = activity; } @Override protected void onPreExecute() { mDialog = new ProgressDialog(mActivity); mDialog.setMessage(mActivity.getString(R.string.loading)); mDialog.show(); } @Override protected Void doInBackground(ITaskEntity... params) { mITaskEntity = params[0]; mITaskEntity.backgroundProc(); return null; } @Override protected void onPostExecute(Void v) { mITaskEntity.postProc(); mDialog.dismiss(); mDialog = null; } }
コンストラクタでは引数で受け取ったアクティビティをメンバ変数に保持し、onPreExecuteでプログレスダイアログを表示する際に使用しています。
doInBackgroundでITaskEntityを受け取って保持し、バックグラウンド処理を呼び出します。バックグラウンド処理が終了し、onPostExecuteが呼び出されたらITaskEntityを介して後処理を実行、プログレスダイアログの消去を行います。
これでメイン画面を作成する準備が整いました。自動生成されたRSSReaderActivityに必要なメンバ変数などを追加していきます(リスト4-24)。
public class RSSReaderActivity extends ListActivity implements View.OnClickListener, ITaskEntity { private FeedList mList = null; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.main); Button button; button = (Button) findViewById(R.id.Get); button.setOnClickListener(this); button = (Button) findViewById(R.id.Config); button.setOnClickListener(this); }
自動生成されたRSSReaderActivityはベースクラスがActivityとなっているので、まずはこれをListActivityに変更します。app.android.Activityをimportしている行も併せてListActivityに変更して下さい。また、このアクテビティでボタンクリックとHTTP通信を処理するため、View.OnClickListenerとITaskEntityをimplementsします。
メンバ変数mListは、HTTP通信により取得したフィードデータを保持するFeedListです。
onCreateで取得ボタンと設定ボタンをOnClickListenerとして登録し、以下のonClickメソッドで処理を行います(リスト4-25)。
@Override public void onClick(View view) { int id = view.getId(); switch (id) { case R.id.Get: mList = null; HttpAccessTask task = new HttpAccessTask(this); task.execute(this); break; case R.id.Config: showConfig(); break; } }
取得(Get)ボタンではリストを一旦クリアして、HttpAccessTaskを実行しています。これにより、ITaskEntityで宣言されたメソッドbackgroundProcとpostProcが呼び出されます(リスト4-26)。一方の設定(Config)ボタンは後ほど実装するshowConfigメソッド(リスト4-27)により設定画面アクティビティの呼び出しを行います。
@Override public void backgroundProc() { RSSReaderApplication app = (RSSReaderApplication) getApplication(); mList = new FeedList(); mList.get(app); } @Override public void postProc() { if (mList.count() > 0) { ArrayList<FeedItem> list = mList.getList(); if (list != null) { FeedAdapter adapter = new FeedAdapter(this, list); setListAdapter(adapter); } } else { Toast.makeText(this, "* NOT FOUND *", Toast.LENGTH_SHORT).show(); } }
backgroundProcはHttpAccessTaskによりバックグラウンドスレッドから呼び出されます。ここでFeedListを生成して、getメソッドによりRSSフィードを取得します。取得するフィードのURLを得るために、RSSReaderApplicationクラスのインスタンスをgetメソッドに渡しています。
postProcはバックグラウンド処理が終了したあと、後処理として呼び出されます。FeedListに登録された件数をチェックし、一件でもデータがあればFeedItemのArrayListをFeedListから取得し、これを渡して生成したFeedAdapterをsetListAdapterメソッドに渡しています。
setListAdapterはListActivityのメソッドで、統合されたリストビューに対してアダプターを設定します。これはListViewに対して直接setAdapterする代わりに利用出来ます。
HTTP通信の結果、フィードが一件も取得出来なかった場合には「* NOT FOUND *」というToastを表示しています。
private void showConfig() { Intent intent = new Intent(getApplicationContext(), com.beatcraft.rssreader.ConfigActivity.class); startActivity(intent); }
showConfigメソッドでは設定画面アクティビティ(ConfigActivity)を、明示的なインテントで呼び出しています。
これでメイン画面の実装はひとまず完了です。RSSReaderのマニフェストファイル(AndroidManifest.xml)を開き、アプリケーションクラスとアクティビティ、パーミッションの追加を行います(リスト4-28)。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.beatcraft.rssreader" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="7" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:name="com.beatcraft.rssreader.RSSReaderApplication" > <activity android:label="@string/app_name" android:name=".RSSReaderActivity" > <intent-filter > <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:label="@string/app_name" android:name=".ConfigActivity" /> </application> <uses-permission android:name="android.permission.INTERNET" > </uses-permission> </manifest>
まずはapplication要素にRSSReaderApplicationを追加するため、属性android:name="com.beatcraft.rssreader.RSSReaderApplication"を追加しています。
それから設定画面を呼び出し可能とするためのConfigActivityのactivity要素、android.permission.INTERNETというパーミッションをそれぞれ追加すれば完了です。
それでは一度実行してみましょう。設定画面で取得フィードのURLを登録し、メイン画面で「取得」ボタンをクリックすれば、図4-1のように一覧表示が行われるはずです。
続いて詳細画面アクティビティを作成し、一覧から記事をタップして遷移するよう変更を加えます。android.app.ActivityをベースクラスとしてDescActivityを生成し、いくつかの定数とonCreateメソッドを追加します(リスト4-29)。ボタンのクリック処理をこのクラスで行うため、View.OnClickListenerのimplementsも追加して下さい。
public class DescActivity extends Activity implements View.OnClickListener { private static final String WEBVIEW_BEGIN = "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" /></head><body bgcolor=\"#f6f6f6\">"; private static final String WEBVIEW_LINK = "<p><a href=\"%s\">%s</a></p>"; private static final String WEBVIEW_END = "</body></html>"; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.description); Button button = (Button) findViewById(R.id.Back); button.setOnClickListener(this); Intent intent = getIntent(); if (intent != null) { String tmp = ""; String desc = ""; TextView tv; tv = (TextView) findViewById(R.id.FeedTitle); tmp = intent.getStringExtra("FeedTitle"); tv.setText(tmp); tv = (TextView) findViewById(R.id.ArticleTitle); tmp = intent.getStringExtra("ArticleTitle"); tv.setText(tmp); tv = (TextView) findViewById(R.id.PubDate); tmp = intent.getStringExtra("PubDate"); tv.setText(tmp); WebView wv = (WebView) findViewById(R.id.Description); desc = WEBVIEW_BEGIN; tmp = intent.getStringExtra("Link"); desc += String.format(WEBVIEW_LINK, tmp, tmp); tmp = intent.getStringExtra("Description"); desc += tmp + WEBVIEW_END; wv.loadDataWithBaseURL("about:blank", desc, "text/html", "utf-8", null); } }
このアクティビティもrequestWindowFeatureでタイトル行を非表示にし、レイアウトには最初に作成したレイアウトのうちdescriptionレイアウトを指定します。
戻る(Back)ボタンのOnClickListenerを設定したあと、getIntentメソッドでこのアクティビティを呼び出したインテントを取得しています。このインテントに含まれる拡張データがこのアクティビティで表示する記事詳細になります。
インテントから「FeedTitle(RSSフィードのタイトル)」「ArticleTitle(記事のタイトル)」「PubDate(出版日時)」という文字列データを取得し、同名のTextViewに設定します。
次にこのクラスに追加した定数を利用して、WebView「Description」に設定するHTMLを整形します。
HTMLはWEBVIEW_BEGINで始まり、続いてWEBVIEW_LINKを追加します。この際、WEBVIEW_LINKをフォーマットしてインテントから取得した「Link(記事本文へのリンクURL)」を埋め込みます。
続いて、同様に取得した文字列データ「Description(詳細文)」を追加し、最後にWEBVIEW_ENDでHTML文書を閉じます。
こうして整形したHTML文書をWebViewのloadDataWithBaseURLで設定することで、本文へのリンクが上部に持ち、HTMLタグを含むフィード詳細文が埋め込みブラウザ内に表示されます。
@Override public void onClick(View view) { if (view.getId() == R.id.Back) { finish(); } }
onClickメソッド(リスト4-30)で戻る(Back)ボタンの処理を追加すれば、このアクティビティは完成です。
一覧からの記事タップでこのアクティビティを呼び出すため、RSSReaderActivityのonCreateメソッドに以下の行を追加します(リスト4-31)。
getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int pos, long id) { FeedItem item = (FeedItem) getListView().getItemAtPosition(pos); Intent intent = new Intent(getApplicationContext(), com.beatcraft.rssreader.DescActivity.class); intent.putExtra("FeedTitle", item.feedTitle()); intent.putExtra("ArticleTitle", item.articleTitle()); intent.putExtra("PubDate", item.pubDate()); intent.putExtra("Description", item.description()); intent.putExtra("Link", item.link()); startActivity(intent); } });
getListViewでListActivityの埋め込みリストビューを取得し、OnItemClickListenerを登録します。登録するリスナーのonItemClickメソッドでは、getItemAtPositionでタップされた行を取得出来ます。FeedAdapterのisEnabledでアイテムタイプがITEMTYPE_FEEDCHANNELの場合は無効としているため、ここで取得出来るFeedItemは必ず記事データ(ITEMTYPE_FEEDITEM)になります。
DescActivityを呼び出す明示的なインテントを生成したあと、取得したFeedItemからRSSフィードのタイトル、記事タイトル、出版日時、詳細文、リンクURLを取得して拡張データに設定しています。
このインテントをstartActivityに渡せば、タップした記事を表示する詳細画面アクティビティが開きます。
最後に詳細画面アクティビティをインテントで呼び出せるようにするため、マニフェストファイルに以下を追加します(リスト4-32)。
<activity android:label="@string/app_name" android:name=".DescActivity" />
以上で完成です。一覧から記事をタップすると、該当記事の詳細画面が表示されることを確認して下さい。埋め込みブラウザに記事本文へのリンクを表示しているので、こちらのタップで外部ブラウザが起動され、記事本文を表示することも可能です。
ここまでで簡易RSSリーダーは完成しましたが、RSSフィードのURLを一件ずつ手入力しなくてはならず、HTTP通信としてもGETメソッドしか使用していません。希望ジャンルのお勧めRSSを三件ずつXMLで返すサイトを用意しましたので、こちらを利用してアプリケーションを拡張していきます。
URL: http://labs.beatcraft.com/ja/androidtext/recommend.php BASICユーザー: beatandroid BASICパスワード: sample
上記URLにPOST変数genreとして、news/music/movieいずれかを渡すと、ニュース/音楽/映画ジャンルのお勧めRSSを返します。リスト4-33が呼び出し結果のサンプルになります。
<?xml version="1.0" encoding="UTF-8"?> <feedlist> <genre>news</genre> <feed> <title>CNN</title> <url>http://rss.cnn.com/rss/edition.rss</url> </feed> <feed> <title>The Wall Street Journal</title> <url>http://online.wsj.com/xml/rss/3_7480.xml</url> </feed> <feed> <title>Reuters</title> <url>http://feeds.reuters.com/reuters/topNews?format=xml</url> </feed> </feedlist>
まずはstrings.xmlに、お勧め選択画面で使用する文字列リソースを追加します(リスト4-34)。
<string name="recommend">お勧め検索</string> <string name="search">検索</string> <string name="addfeed">チェックしたフィードを追加</string> <string name="label_genre">ジャンル:</string> <string-array name="genre"> <item>ニュース</item> <item>音楽</item> <item>映画</item> </string-array>
続いてレイアウトを作成して行きましょう。recommend.xmlというレイアウトファイルを作成し、リスト4-35のように編集します。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/linearLayout1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dip" > <TextView android:id="@+id/LabelGenre" android:layout_width="wrap_content" android:layout_height="fill_parent" android:gravity="center_vertical" android:text="@string/label_genre" /> <Spinner android:id="@+id/Genre" android:layout_width="120dip" android:layout_height="wrap_content" /> <Button android:id="@+id/Search" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_weight="1" android:text="@string/search" /> </LinearLayout> <Button android:id="@+id/Back" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="@string/back" /> <CheckBox android:id="@+id/Feed01" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="CheckBox" android:visibility="invisible" /> <CheckBox android:id="@+id/Feed02" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="CheckBox" android:visibility="invisible" /> <CheckBox android:id="@+id/Feed03" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="CheckBox" android:visibility="invisible" /> <Button android:id="@+id/AddFeed" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_margin="5dip" android:text="@string/addfeed" android:visibility="invisible" /> </LinearLayout>
お勧めRSSを取得し、画面に設定したものが図4-4になります。
最上部にはジャンル選択のドロップダウンリスト(Spinner)と検索ボタンが並び、その下には設定画面に戻るボタンが置かれています。
それに続いてお勧めRSSのチェックボックスが三件並び、最後にチェックしたフィードを取得フィードURLに追加するボタンが続きます。これらは初期状態では非表示(android:visibility="invisible")に設定されていることに注意して下さい。
続いてこのお勧め画面を呼び出すため、設定画面にボタンを一つ追加します。config.xmlを開き、取得件数ドロップダウンリストの横にお勧め検索ボタンを追加します(リスト4-36、図4-5)。
<LinearLayout android:id="@+id/linearLayout1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dip" > <TextView android:id="@+id/LabelNumOfGet" android:layout_width="wrap_content" android:layout_height="fill_parent" android:gravity="center_vertical" android:text="@string/label_number_of_get" /> <Spinner android:id="@+id/NumOfGet" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:id="@+id/Recommend" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_weight="1" android:text="@string/recommend" /> </LinearLayout>
お勧め選択画面の実装を進めていきましょう。android.app.ActivityをベースクラスとしてRecommendActivityを生成し、いくつかの定数とメンバ変数を追加します(リスト4-37)。ボタンのクリック処理とHttpAccessTaskの処理をこのクラスで行うため、View.OnClickListenerとITaskEntityのimplementsも追加して下さい。
public class RecommendActivity extends Activity implements View.OnClickListener, ITaskEntity { private static final String GENRE[] = {"news", "music", "movie"}; private static final String POST_DOMAIN = "labs.beatcraft.com"; private static final String POST_PATH = "/ja/androidtext/recommend.php"; private static final String POST_USER = "beatandroid"; private static final String POST_PASS = "sample"; private static final int NUM_OF_RECOMMEND = 3; private Spinner mGenre; private CheckBox mFeedCheck[]; private String mRecommendTitle[]; private String mRecommendURL[]; private Button mAddFeed; private int mStat = -1;
GENREはお勧めRSSのジャンルとして選択可能な値を格納する定数配列で、ドロップダウンリスト(Spinner)の選択内容に対応したnews(ニュース)/music(音楽)/movie(映画)が設定されています。
POST_DOMAINとPOST_PATHは今回用意したお勧めRSS取得用サイトのドメインとパスになります。BASIC認証がかかっているため、認証用のユーザー/パスワードがPOST_USERとPOST_PASSとして定義されています。
NUM_OF_RECOMMENDは各ジャンルでお勧めとして取得可能なRSSの数で、画面のチェックボックスにあわせ3が設定されています。
メンバ変数のうち、mRecommendTitleは取得したお勧めRSSのタイトルを保持する配列で、同様にmRecommendURLにはお勧めRSSのURLが格納されます。
/** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.recommend); ArrayAdapter<CharSequence> adapter; adapter = ArrayAdapter.createFromResource(this, R.array.genre, android.R.layout.simple_spinner_item); adapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); mGenre = (Spinner) findViewById(R.id.Genre); mGenre.setAdapter(adapter); Button button; button = (Button) findViewById(R.id.Search); button.setOnClickListener(this); button = (Button) findViewById(R.id.Back); button.setOnClickListener(this); mAddFeed = (Button) findViewById(R.id.AddFeed); mAddFeed.setOnClickListener(this); mFeedCheck = new CheckBox[NUM_OF_RECOMMEND]; mFeedCheck[0] = (CheckBox) findViewById(R.id.Feed01); mFeedCheck[1] = (CheckBox) findViewById(R.id.Feed02); mFeedCheck[2] = (CheckBox) findViewById(R.id.Feed03); mRecommendTitle = new String[NUM_OF_RECOMMEND]; mRecommendURL = new String[NUM_OF_RECOMMEND]; }
onCreateメソッド(リスト4-38)でタイトル行の非表示化とrecommendレイアウトの設定を行ったあと、mGenreに格納したSpinnerに文字列配列リソースgenreをセットしたArrayAdapterを設定します。
検索(Search)ボタン、戻る(Back)ボタン、チェックしたフィードの追加(AddFeed)ボタンのOnClickListenerをこのクラスに設定し、チェックボックスの保持とお勧め格納用配列の生成を行なっています。
@Override public void onClick(View view) { int id = view.getId(); switch (id) { case R.id.Search: HttpAccessTask task = new HttpAccessTask(this); task.execute(this); break; case R.id.Back: finish(); break; case R.id.AddFeed: addFeed(); break; } }
onClickメソッド(リスト4-39)では検索(Search)ボタンによるHttpAccessTaskの実行、戻る(Back)ボタンによるアクティビティの終了、チェックしたフィードを追加(AddFeed)ボタンによるaddFeedメソッド(後述)の呼び出しを行います。
続いてHttpAccessTaskの実行により呼び出されるbackgroundProcとpostProcを実装します(リスト4-40)。
@Override public void backgroundProc() { for (int i = 0; i < NUM_OF_RECOMMEND; ++i) { mRecommendTitle[i] = ""; mRecommendURL[i] = ""; } mStat = get(GENRE[mGenre.getSelectedItemPosition()]); } @Override public void postProc() { if (mStat == 0) { for (int i = 0; i < NUM_OF_RECOMMEND; ++i) { mFeedCheck[i].setText(mRecommendTitle[i]); mFeedCheck[i].setVisibility(View.VISIBLE); mFeedCheck[i].setChecked(false); } mAddFeed.setVisibility(View.VISIBLE); } else { for (int i = 0; i < NUM_OF_RECOMMEND; ++i) { mFeedCheck[i].setText(""); mFeedCheck[i].setVisibility(View.INVISIBLE); mFeedCheck[i].setChecked(false); } mAddFeed.setVisibility(View.INVISIBLE); } }
backgroundProcではお勧めRSSを保持する各配列をクリアし、getメソッドを呼び出しています。この際、ドロップダウンリストmGenreで選択されたインデックスを元に文字列配列GENREを参照し、取得したいジャンルを指定しています。またgetメソッドの取得正否を判定するため、戻り値をメンバ変数mStatに格納します。
postProcではこのmStatをチェックし、0(成功)であればチェックボックスにお勧めRSSのタイトルを設定し、追加ボタンとあわせて表示状態にしています。失敗した場合は各チェックボックスのテキストをクリアして、追加ボタンとあわせ非表示にします。
private int get(String genre) { String url = "http://" + POST_DOMAIN + POST_PATH; DefaultHttpClient client = new DefaultHttpClient(); if (client != null) { client.getParams().setParameter("http.socket.timeout", new Integer(15000)); HttpPost method = null; try { method = new HttpPost(url); } catch (Exception e) { e.printStackTrace(); } if (method == null) { return -1; } HttpResponse response = null; try { List<NameValuePair> pair = new ArrayList<NameValuePair>(); pair.add(new BasicNameValuePair("genre", genre)); method.setEntity(new UrlEncodedFormEntity(pair, HTTP.UTF_8)); Credentials cred = new UsernamePasswordCredentials(POST_USER, POST_PASS); client.getCredentialsProvider().setCredentials( new AuthScope(POST_DOMAIN, 80), cred); response = client.execute(method); int ret = response.getStatusLine().getStatusCode(); if (ret == HttpStatus.SC_OK) { InputStream is = response.getEntity().getContent(); return parse(is); } } catch (Exception e) { e.printStackTrace(); } finally { client.getConnectionManager().shutdown(); } } return -1; }
リスト4-41がgetメソッドになります。基本的な内容はRSSフィードを取得するFeedList.getと同様ですが、HTTP通信で使用するメソッドクラスとしてHttpGetではなく、HttpPostを使用しています。
また、生成されたHttpPostのインスタンスに対し、List<NameValuePair>の変数pairを使用してPOST変数を設定しています。pairのキーとしてPOST変数名である"genre"を、値としてget呼び出し時に引数として受け取った変数genreを設定し、これを基にUrlEncodedFormEntityを生成、HttpPost.setEntityに渡しています(リスト4-42)。
これにより、ジャンルの選択リストから選ばれたニュース(news)/音楽(music)/映画(movie)といった値がHTTP POSTの変数genreとして渡されます。
List<NameValuePair> pair = new ArrayList<NameValuePair>(); pair.add(new BasicNameValuePair("genre", genre)); method.setEntity(new UrlEncodedFormEntity(pair, HTTP.UTF_8));
続く二行で、BASIC認証のユーザーとパスワードをHTTPクライアントにセットしています(リスト4-43)。
Credentials cred = new UsernamePasswordCredentials(POST_USER, POST_PASS); client.getCredentialsProvider().setCredentials(new AuthScope(POST_DOMAIN, 80), cred);
まずBASIC認証のユーザー名(定数POST_USER)、パスワード(定数POST_PASS)によりUsernamePasswordCredentialsを生成します。
この認証情報をHTTPクライアントのgetCredentialsProviderで取得した認証プロバイダに渡すと、execute時にBASIC認証が行われます。この際、認証のスコープとして定数POST_DOMAINとポート番号80を渡しています。
getから呼び出されるparseメソッド(リスト4-44)では、フィードの取得同様にXmlPullParserを利用して解析を行なっています。
private int parse(InputStream is) { int count = 0; boolean inFeed = false; XmlPullParser p = Xml.newPullParser(); try { p.setInput(is, null); int event = p.getEventType(); while (event != XmlPullParser.END_DOCUMENT) { String elem = null; String tmp = null; switch (event) { case XmlPullParser.START_TAG: elem = p.getName(); if (elem.equals("feed") == true) { if (inFeed == true) { count++; if (count >= NUM_OF_RECOMMEND) { return 0; } } inFeed = true; } else if (elem.equals("title") == true) { tmp = p.nextText(); if (tmp != null) { mRecommendTitle[count] = tmp; } } else if (elem.equals("url") == true) { tmp = p.nextText(); if (tmp != null) { mRecommendURL[count] = tmp; } } break; case XmlPullParser.END_TAG: elem = p.getName(); if (elem.equals("feed") == true) { count++; if (count >= NUM_OF_RECOMMEND) { return 0; } inFeed = false; } break; } event = p.next(); } } catch (Exception e) { e.printStackTrace(); return -1; } return 0; }
お勧めRSSのタイトルとURLが取得出来たら、それぞれメンバ変数mRecommendTitleとmRecommendURLに保持しています。このメソッドはバックグラウンドスレッドで呼び出されるため、直接チェックボックスにタイトルを設定することは出来ません。
最後にチェックしたフィードを追加(AddFeed)ボタンで呼び出されるaddFeedメソッド(リスト4-45)を実装します。
private void addFeed() { int count = 0; Intent result = new Intent(); for (int i = 0; i < NUM_OF_RECOMMEND; ++i) { if (mFeedCheck[i].isChecked() == true) { String key = ""; key = String.format("Recommend%02d", (count + 1)); result.putExtra(key, mRecommendURL[i]); count++; } } result.putExtra("RecommendCount", count); setResult(RESULT_OK, result); finish(); }
addFeedではresultというインテントを生成し、チェックされたRSSのURLとその数を「RecommendXX」(XXは01〜03)、「RecommendCount」というキーで拡張データとして追加しています。
このインテントをsetResultメソッドに定数RESULT_OKとともにセットしたあと、アクティビティを終了しています。このsetResultにより、このアクティビティを呼び出したアクティビティに呼び出し結果を返すことが出来ます。
以上でお勧め選択画面の実装は完了です。次は設定画面を修正し、これを呼び出せるようにしていきます。
ConfigActivityを開き、詳細画面呼び出しで使用する定数を追加します。
public static final int REQUEST_RECOMMEND = 1234;
onCreateメソッドの最後に以下を追加し、お勧め検索ボタンをonClickでハンドリング出来るようにします。
button = (Button) findViewById(R.id.Recommend); button.setOnClickListener(this);
続いてonClickメソッドのswitch文に、以下のcaseを追加します。
case R.id.Recommend: showRecommend(); break;
リスト4-46がshowRecommendメソッドになります。
private void showRecommend() { Intent intent = new Intent(getApplicationContext(), com.beatcraft.rssreader.RecommendActivity.class); startActivityForResult(intent, REQUEST_RECOMMEND); }
明示的インテントを生成してRecommendActivityを呼び出していますが、startActivityではなくstartActivityForResultを使用している点に注意して下さい。これにより、onActivityResultでお勧め選択画面からお勧めRSSを受け取ることが可能になります。
リスト4-47がonActivityResultになります。
protected void onActivityResult(int reqCode, int result, Intent data) { super.onActivityResult(reqCode, result, data); if (result == RESULT_OK) { if (reqCode == REQUEST_RECOMMEND) { int count = 0; count = data.getIntExtra("RecommendCount", 0); for (int i = 0; i < count; ++i) { String key = ""; String url = ""; key = String.format("Recommend%02d", (i + 1)); url = data.getStringExtra(key); if (url.equals("") == false) { for (int j = 0; j < RSSReaderApplication.NUM_OF_FEED; ++j) { String tmp = mFeedURL[j].getText().toString().trim(); if (tmp.equals("") == true) { mFeedURL[j].setText(url); break; } } } } } } }
結果がRESULT_OKで、リクエストコードがREQUEST_RECOMMENDだった場合、渡されたインテントからRecommendCountを取得します。
お勧めRSSが一件でも渡されていれば、これを一つずつ取得して空いている取得フィードURLにセットします。取得フィードURLに三件分をセットする空きがなかった場合は余剰分は切り捨てられます。
最後にマニフェストファイルに以下を追加して、お勧め選択画面を呼び出せるようにしましょう。
<activity android:label="@string/app_name" android:name=".RecommendActivity" />
以上で全ての実装は完了です。実行して動作を確認してみて下さい。
package com.beatcraft.rssreader; import android.app.Application; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; public class RSSReaderApplication extends Application { private static final String CONF_NAME = "rssreader.conf"; public static final int DEFAULT_NUM_OF_GET = 3; public static final int NUM_OF_FEED = 10; private int mNumOfGet = DEFAULT_NUM_OF_GET; private String mFeedURL[]; @Override public void onCreate() { mFeedURL = new String[NUM_OF_FEED]; loadConfig(); } public int numberOfGet() { return mNumOfGet; } public void setNumberOfGet(int numOfGet) { mNumOfGet = numOfGet; } public String feedURL(int index) { if ((index < 0) || (10 <= index)) { return null; } return mFeedURL[index]; } public void setFeedURL(int index, String url) { if ((index < 0) || (NUM_OF_FEED <= index)) { return; } mFeedURL[index] = url; } public void clearFeedURL() { for (int i = 0; i < NUM_OF_FEED; ++i) { mFeedURL[i] = ""; } } public void loadConfig() { SharedPreferences pref = getSharedPreferences(CONF_NAME, MODE_PRIVATE); mNumOfGet = pref.getInt("NumOfGet", DEFAULT_NUM_OF_GET); for (int i = 0; i < NUM_OF_FEED; ++i) { String key = String.format("FeedURL%02d", (i + 1)); mFeedURL[i] = pref.getString(key, ""); } } public void saveConfig() { SharedPreferences pref = getSharedPreferences(CONF_NAME, MODE_PRIVATE); Editor pe = pref.edit(); pe.putInt("NumOfGet", mNumOfGet); for (int i = 0; i < NUM_OF_FEED; ++i) { String key = String.format("FeedURL%02d", (i + 1)); pe.putString(key, mFeedURL[i]); } pe.commit(); } }
package com.beatcraft.rssreader; import java.util.ArrayList; import android.app.ListActivity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.Window; import android.widget.AdapterView; import android.widget.Button; import android.widget.Toast; public class RSSReaderActivity extends ListActivity implements View.OnClickListener, ITaskEntity { private FeedList mList = null; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.main); Button button; button = (Button) findViewById(R.id.Get); button.setOnClickListener(this); button = (Button) findViewById(R.id.Config); button.setOnClickListener(this); getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int pos, long id) { FeedItem item = (FeedItem) getListView().getItemAtPosition(pos); Intent intent = new Intent(getApplicationContext(), com.beatcraft.rssreader.DescActivity.class); intent.putExtra("FeedTitle", item.feedTitle()); intent.putExtra("ArticleTitle", item.articleTitle()); intent.putExtra("PubDate", item.pubDate()); intent.putExtra("Description", item.description()); intent.putExtra("Link", item.link()); startActivity(intent); } }); } @Override public void onClick(View view) { int id = view.getId(); switch (id) { case R.id.Get: mList = null; HttpAccessTask task = new HttpAccessTask(this); task.execute(this); break; case R.id.Config: showConfig(); break; } } @Override public void backgroundProc() { RSSReaderApplication app = (RSSReaderApplication) getApplication(); mList = new FeedList(); mList.get(app); } @Override public void postProc() { if (mList.count() > 0) { ArrayList<FeedItem> list = mList.getList(); if (list != null) { FeedAdapter adapter = new FeedAdapter(this, list); setListAdapter(adapter); } } else { Toast.makeText(this, "* NOT FOUND *", Toast.LENGTH_SHORT).show(); } } private void showConfig() { Intent intent = new Intent(getApplicationContext(), com.beatcraft.rssreader.ConfigActivity.class); startActivity(intent); } }
package com.beatcraft.rssreader; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.Window; import android.webkit.WebView; import android.widget.Button; import android.widget.TextView; public class DescActivity extends Activity implements View.OnClickListener { private static final String WEBVIEW_BEGIN = "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" /></head><body bgcolor=\"#f6f6f6\">"; private static final String WEBVIEW_LINK = "<p><a href=\"%s\">%s</a></p>"; private static final String WEBVIEW_END = "</body></html>"; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.description); Button button = (Button) findViewById(R.id.Back); button.setOnClickListener(this); Intent intent = getIntent(); if (intent != null) { String tmp = ""; String desc = ""; TextView tv; tv = (TextView) findViewById(R.id.FeedTitle); tmp = intent.getStringExtra("FeedTitle"); tv.setText(tmp); tv = (TextView) findViewById(R.id.ArticleTitle); tmp = intent.getStringExtra("ArticleTitle"); tv.setText(tmp); tv = (TextView) findViewById(R.id.PubDate); tmp = intent.getStringExtra("PubDate"); tv.setText(tmp); WebView wv = (WebView) findViewById(R.id.Description); desc = WEBVIEW_BEGIN; tmp = intent.getStringExtra("Link"); desc += String.format(WEBVIEW_LINK, tmp, tmp); tmp = intent.getStringExtra("Description"); desc += tmp + WEBVIEW_END; wv.loadDataWithBaseURL("about:blank", desc, "text/html", "utf-8", null); } } @Override public void onClick(View view) { if (view.getId() == R.id.Back) { finish(); } } }
package com.beatcraft.rssreader; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.Window; import android.view.WindowManager.LayoutParams; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.Spinner; public class ConfigActivity extends Activity implements View.OnClickListener { public static final int REQUEST_RECOMMEND = 1234; private static final int NUM_OF_GET_LIST[] = {1, 3, 5}; private Spinner mNumOfGet; private EditText mFeedURL[]; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); this.getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); setContentView(R.layout.config); RSSReaderApplication app = (RSSReaderApplication) getApplication(); ArrayAdapter<CharSequence> adapter; adapter = ArrayAdapter.createFromResource(this, R.array.number_of_get, android.R.layout.simple_spinner_item); adapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); mNumOfGet = (Spinner) findViewById(R.id.NumOfGet); mNumOfGet.setAdapter(adapter); for (int i = 0; i < 3; ++i) { if (NUM_OF_GET_LIST[i] == app.numberOfGet()) { mNumOfGet.setSelection(i); break; } } mFeedURL = new EditText[RSSReaderApplication.NUM_OF_FEED]; mFeedURL[0] = (EditText) findViewById(R.id.FeedURL01); mFeedURL[1] = (EditText) findViewById(R.id.FeedURL02); mFeedURL[2] = (EditText) findViewById(R.id.FeedURL03); mFeedURL[3] = (EditText) findViewById(R.id.FeedURL04); mFeedURL[4] = (EditText) findViewById(R.id.FeedURL05); mFeedURL[5] = (EditText) findViewById(R.id.FeedURL06); mFeedURL[6] = (EditText) findViewById(R.id.FeedURL07); mFeedURL[7] = (EditText) findViewById(R.id.FeedURL08); mFeedURL[8] = (EditText) findViewById(R.id.FeedURL09); mFeedURL[9] = (EditText) findViewById(R.id.FeedURL10); for (int i = 0; i < RSSReaderApplication.NUM_OF_FEED; ++i) { mFeedURL[i].setText(app.feedURL(i)); } Button button; button = (Button) findViewById(R.id.Recommend); button.setOnClickListener(this); button = (Button) findViewById(R.id.Cancel); button.setOnClickListener(this); button = (Button) findViewById(R.id.Regist); button.setOnClickListener(this); } @Override public void onClick(View view) { int id = view.getId(); switch (id) { case R.id.Recommend: showRecommend(); break; case R.id.Cancel: finish(); break; case R.id.Regist: regist(); break; } } protected void onActivityResult(int reqCode, int result, Intent data) { super.onActivityResult(reqCode, result, data); if (result == RESULT_OK) { if (reqCode == REQUEST_RECOMMEND) { int count = 0; count = data.getIntExtra("RecommendCount", 0); for (int i = 0; i < count; ++i) { String key = ""; String url = ""; key = String.format("Recommend%02d", (i + 1)); url = data.getStringExtra(key); if (url.equals("") == false) { for (int j = 0; j < RSSReaderApplication.NUM_OF_FEED; ++j) { String tmp = mFeedURL[j].getText().toString().trim(); if (tmp.equals("") == true) { mFeedURL[j].setText(url); break; } } } } } } } private void showRecommend() { Intent intent = new Intent(getApplicationContext(), com.beatcraft.rssreader.RecommendActivity.class); startActivityForResult(intent, REQUEST_RECOMMEND); } private void regist() { RSSReaderApplication app = (RSSReaderApplication) getApplication(); app.setNumberOfGet(NUM_OF_GET_LIST[mNumOfGet.getSelectedItemPosition()]); app.clearFeedURL(); for (int i = 0, j = 0; i < RSSReaderApplication.NUM_OF_FEED; ++i) { String url = mFeedURL[i].getText().toString().trim(); if (url.equals("") == false) { app.setFeedURL(j++, url); } } app.saveConfig(); finish(); } }
package com.beatcraft.rssreader; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HTTP; import org.xmlpull.v1.XmlPullParser; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Xml; import android.view.View; import android.view.Window; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.Spinner; public class RecommendActivity extends Activity implements View.OnClickListener, ITaskEntity { private static final String GENRE[] = {"news", "music", "movie"}; private static final String POST_DOMAIN = "labs.beatcraft.com"; private static final String POST_PATH = "/ja/androidtext/recommend.php"; private static final String POST_USER = "beatandroid"; private static final String POST_PASS = "sample"; private static final int NUM_OF_RECOMMEND = 3; private Spinner mGenre; private CheckBox mFeedCheck[]; private String mRecommendTitle[]; private String mRecommendURL[]; private Button mAddFeed; private int mStat = -1; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.recommend); ArrayAdapter<CharSequence> adapter; adapter = ArrayAdapter.createFromResource(this, R.array.genre, android.R.layout.simple_spinner_item); adapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); mGenre = (Spinner) findViewById(R.id.Genre); mGenre.setAdapter(adapter); Button button; button = (Button) findViewById(R.id.Search); button.setOnClickListener(this); button = (Button) findViewById(R.id.Back); button.setOnClickListener(this); mAddFeed = (Button) findViewById(R.id.AddFeed); mAddFeed.setOnClickListener(this); mFeedCheck = new CheckBox[NUM_OF_RECOMMEND]; mFeedCheck[0] = (CheckBox) findViewById(R.id.Feed01); mFeedCheck[1] = (CheckBox) findViewById(R.id.Feed02); mFeedCheck[2] = (CheckBox) findViewById(R.id.Feed03); mRecommendTitle = new String[NUM_OF_RECOMMEND]; mRecommendURL = new String[NUM_OF_RECOMMEND]; } @Override public void onClick(View view) { int id = view.getId(); switch (id) { case R.id.Search: HttpAccessTask task = new HttpAccessTask(this); task.execute(this); break; case R.id.Back: finish(); break; case R.id.AddFeed: addFeed(); break; } } @Override public void backgroundProc() { for (int i = 0; i < NUM_OF_RECOMMEND; ++i) { mRecommendTitle[i] = ""; mRecommendURL[i] = ""; } mStat = get(GENRE[mGenre.getSelectedItemPosition()]); } @Override public void postProc() { if (mStat == 0) { for (int i = 0; i < NUM_OF_RECOMMEND; ++i) { mFeedCheck[i].setText(mRecommendTitle[i]); mFeedCheck[i].setVisibility(View.VISIBLE); mFeedCheck[i].setChecked(false); } mAddFeed.setVisibility(View.VISIBLE); } else { for (int i = 0; i < NUM_OF_RECOMMEND; ++i) { mFeedCheck[i].setText(""); mFeedCheck[i].setVisibility(View.INVISIBLE); mFeedCheck[i].setChecked(false); } mAddFeed.setVisibility(View.INVISIBLE); } } private int get(String genre) { String url = "http://" + POST_DOMAIN + POST_PATH; DefaultHttpClient client = new DefaultHttpClient(); if (client != null) { client.getParams().setParameter("http.socket.timeout", new Integer(15000)); HttpPost method = null; try { method = new HttpPost(url); } catch (Exception e) { e.printStackTrace(); } if (method == null) { return -1; } HttpResponse response = null; try { List<NameValuePair> pair = new ArrayList<NameValuePair>(); pair.add(new BasicNameValuePair("genre", genre)); method.setEntity(new UrlEncodedFormEntity(pair, HTTP.UTF_8)); Credentials cred = new UsernamePasswordCredentials(POST_USER, POST_PASS); client.getCredentialsProvider().setCredentials(new AuthScope(POST_DOMAIN, 80), cred); response = client.execute(method); int ret = response.getStatusLine().getStatusCode(); if (ret == HttpStatus.SC_OK) { InputStream is = response.getEntity().getContent(); return parse(is); } } catch (Exception e) { e.printStackTrace(); } finally { client.getConnectionManager().shutdown(); } } return -1; } private int parse(InputStream is) { int count = 0; boolean inFeed = false; XmlPullParser p = Xml.newPullParser(); try { p.setInput(is, null); int event = p.getEventType(); while (event != XmlPullParser.END_DOCUMENT) { String elem = null; String tmp = null; switch (event) { case XmlPullParser.START_TAG: elem = p.getName(); if (elem.equals("feed") == true) { if (inFeed == true) { count++; if (count >= NUM_OF_RECOMMEND) { return 0; } } inFeed = true; } else if (elem.equals("title") == true) { tmp = p.nextText(); if (tmp != null) { mRecommendTitle[count] = tmp; } } else if (elem.equals("url") == true) { tmp = p.nextText(); if (tmp != null) { mRecommendURL[count] = tmp; } } break; case XmlPullParser.END_TAG: elem = p.getName(); if (elem.equals("feed") == true) { count++; if (count >= NUM_OF_RECOMMEND) { return 0; } inFeed = false; } break; } event = p.next(); } } catch (Exception e) { e.printStackTrace(); return -1; } return 0; } private void addFeed() { int count = 0; Intent result = new Intent(); for (int i = 0; i < NUM_OF_RECOMMEND; ++i) { if (mFeedCheck[i].isChecked() == true) { String key = ""; key = String.format("Recommend%02d", (count + 1)); result.putExtra(key, mRecommendURL[i]); count++; } } result.putExtra("RecommendCount", count); setResult(RESULT_OK, result); finish(); } }
package com.beatcraft.rssreader; public class FeedItem { public static final int ITEMTYPE_FEEDCHANNEL = 0; public static final int ITEMTYPE_FEEDITEM = 1; private int mItemType; private String mFeedTitle = ""; private String mArticleTitle = ""; private String mPubDate = ""; private String mDescription = ""; private String mLink = ""; public FeedItem(int itemType) { mItemType = itemType; } public int itemType() { return mItemType; } public String feedTitle() { return mFeedTitle; } public void setFeedTitle(String title) { mFeedTitle = title; } public String articleTitle() { return mArticleTitle; } public void setArticleTitle(String title) { mArticleTitle = title; } public String pubDate() { return mPubDate; } public void setPubDate(String pubDate) { mPubDate = pubDate; } public String description() { return mDescription; } public void setDescription(String description) { mDescription = description; } public String link() { return mLink; } public void setLink(String link) { mLink = link; } }
package com.beatcraft.rssreader; import java.io.InputStream; import java.util.ArrayList; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.xmlpull.v1.XmlPullParser; import android.util.Xml; public class FeedList { private ArrayList<FeedItem> mList = null; public FeedList() { mList = new ArrayList<FeedItem>(); } public ArrayList<FeedItem> getList() { return mList; } public int count() { if (mList != null) { return mList.size(); } return 0; } public int get(RSSReaderApplication app) { int success = 0; for (int i = 0; i < RSSReaderApplication.NUM_OF_FEED; ++i) { String url = app.feedURL(i); if (url.equals("") == true) { continue; } DefaultHttpClient client = new DefaultHttpClient(); if (client != null) { client.getParams().setParameter("http.socket.timeout", new Integer(15000)); HttpGet method = null; try { method = new HttpGet(url); } catch (Exception e) { e.printStackTrace(); } if (method == null) { continue; } HttpResponse response = null; try { response = client.execute(method); int ret = response.getStatusLine().getStatusCode(); if (ret == HttpStatus.SC_OK) { InputStream is = response.getEntity().getContent(); if (parse(is, app.numberOfGet()) > 0) { success++; } is.close(); } } catch (Exception e) { e.printStackTrace(); } finally { client.getConnectionManager().shutdown(); } } } return success; } private int parse(InputStream is, int max) { int count = 0; boolean inChannel = false; boolean inItem = false; FeedItem item = null; String feedTitle = ""; XmlPullParser p = Xml.newPullParser(); try { p.setInput(is, null); int event = p.getEventType(); while (event != XmlPullParser.END_DOCUMENT) { String elem = null; String tmp = null; switch (event) { case XmlPullParser.START_TAG: elem = p.getName(); if (elem.equals("channel") == true) { inChannel = true; item = new FeedItem(FeedItem.ITEMTYPE_FEEDCHANNEL); } else if (elem.equals("item") == true) { if (inChannel == true) { if (item != null) { mList.add(item); item = null; count++; } inChannel = false; } inItem = true; item = new FeedItem(FeedItem.ITEMTYPE_FEEDITEM); item.setFeedTitle(feedTitle); } else if (elem.equals("title") == true) { tmp = p.nextText(); if ((tmp != null) && (item != null)) { if (inChannel == true) { feedTitle = tmp; item.setFeedTitle(tmp); } else if (inItem == true) { item.setArticleTitle(tmp); } } } else if ((elem.equals("pubDate") == true) || (elem.equals("date") == true)) { if (inItem == true) { tmp = p.nextText(); if ((tmp != null) && (item != null)) { item.setPubDate(tmp); } } } else if (elem.equals("description") == true) { if (inItem == true) { tmp = p.nextText(); if ((tmp != null) && (item != null)) { item.setDescription(tmp); } } } else if (elem.equals("link") == true) { if (inItem == true) { tmp = p.nextText(); if ((tmp != null) && (item != null)) { item.setLink(tmp); } } } break; case XmlPullParser.END_TAG: elem = p.getName(); if (elem.equals("channel") == true) { if (inChannel == true) { if (item != null) { mList.add(item); item = null; count++; } inChannel = false; } } else if (elem.equals("item") == true) { if (inItem == true) { if (item != null) { mList.add(item); item = null; count++; max--; if (max == 0) { return count; } } inItem = false; } } } event = p.next(); } } catch (Exception e) { e.printStackTrace(); return 0; } return count; } }
package com.beatcraft.rssreader; import java.util.List; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; public class FeedAdapter extends ArrayAdapter<FeedItem> { private LayoutInflater mInflate; public FeedAdapter(Context context, List<FeedItem> obj) { super(context, 0, obj); mInflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public boolean isEnabled(int pos) { FeedItem item = getItem(pos); if (item.itemType() == FeedItem.ITEMTYPE_FEEDCHANNEL) { return false; } return true; } public View getView(final int pos, View convView, ViewGroup parent) { View view = convView; FeedItem item = getItem(pos); switch (item.itemType()) { case FeedItem.ITEMTYPE_FEEDCHANNEL: view = buildChannel(item); break; case FeedItem.ITEMTYPE_FEEDITEM: view = buildItem(item); break; } return view; } private View buildChannel(FeedItem item) { View view = null; view = mInflate.inflate(R.layout.item_channel, null); TextView tv; tv = (TextView) view.findViewById(R.id.FeedTitle); tv.setText(item.feedTitle()); return view; } private View buildItem(FeedItem item) { View view = null; view = mInflate.inflate(R.layout.item_item, null); TextView tv; tv = (TextView) view.findViewById(R.id.ArticleTitle); tv.setText(item.articleTitle()); tv = (TextView) view.findViewById(R.id.PubDate); tv.setText(item.pubDate()); return view; } }
package com.beatcraft.rssreader; public interface ITaskEntity { void backgroundProc(); void postProc(); }
package com.beatcraft.rssreader; import android.app.Activity; import android.app.ProgressDialog; import android.os.AsyncTask; public class HttpAccessTask extends AsyncTask<ITaskEntity, Integer, Void> { private Activity mActivity; private ProgressDialog mDialog; private ITaskEntity mITaskEntity; public HttpAccessTask(Activity activity) { mActivity = activity; } @Override protected void onPreExecute() { mDialog = new ProgressDialog(mActivity); mDialog.setMessage(mActivity.getString(R.string.loading)); mDialog.show(); } @Override protected Void doInBackground(ITaskEntity... params) { mITaskEntity = params[0]; mITaskEntity.backgroundProc(); return null; } @Override protected void onPostExecute(Void v) { mITaskEntity.postProc(); mDialog.dismiss(); mDialog = null; } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/linearLayout1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dip" > <Button android:id="@+id/Get" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_weight="1" android:text="@string/get_feed" /> <Button android:id="@+id/Config" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_weight="1" android:text="@string/config" /> </LinearLayout> <ListView android:id="@+id/android:list" android:layout_width="fill_parent" android:layout_height="wrap_content" android:fastScrollEnabled="true" > </ListView> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#cccccc" android:orientation="vertical" > <TextView android:id="@+id/FeedTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="FeedTitle" android:textColor="#000000" android:textSize="16dip" android:textStyle="bold" /> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:id="@+id/ArticleTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="ArticleTitle" /> <TextView android:id="@+id/PubDate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:layout_margin="5dip" android:text="pubDate" /> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/ScrollView" android:layout_width="fill_parent" android:layout_height="fill_parent" > <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <Button android:id="@+id/Back" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="@string/back" /> <TextView android:id="@+id/FeedTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:text="FeedTitle" android:textSize="16dip" android:textStyle="bold" /> <TextView android:id="@+id/ArticleTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:text="ArticleTitle" /> <TextView android:id="@+id/PubDate" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:layout_marginRight="10dip" android:text="pubDate" /> <android.webkit.WebView android:id="@+id/Description" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_margin="5dip" android:layout_weight="1" > </android.webkit.WebView> </LinearLayout> </ScrollView>
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/ScrollView" android:layout_width="fill_parent" android:layout_height="fill_parent" > <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/linearLayout1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dip" > <TextView android:id="@+id/LabelNumOfGet" android:layout_width="wrap_content" android:layout_height="fill_parent" android:gravity="center_vertical" android:text="@string/label_number_of_get" /> <Spinner android:id="@+id/NumOfGet" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:id="@+id/Recommend" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_weight="1" android:text="@string/recommend" /> </LinearLayout> <EditText android:id="@+id/FeedURL01" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" > <requestFocus /> </EditText> <EditText android:id="@+id/FeedURL02" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL03" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL04" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL05" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL06" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL07" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL08" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL09" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <EditText android:id="@+id/FeedURL10" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip" android:inputType="textUri" /> <LinearLayout android:id="@+id/linearLayout2" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dip" > <Button android:id="@+id/Cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_weight="1" android:text="@string/cancel" /> <Button android:id="@+id/Regist" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_weight="1" android:text="@string/regist" /> </LinearLayout> </LinearLayout> </ScrollView>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/linearLayout1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="10dip" > <TextView android:id="@+id/LabelGenre" android:layout_width="wrap_content" android:layout_height="fill_parent" android:gravity="center_vertical" android:text="@string/label_genre" /> <Spinner android:id="@+id/Genre" android:layout_width="120dip" android:layout_height="wrap_content" /> <Button android:id="@+id/Search" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_weight="1" android:text="@string/search" /> </LinearLayout> <Button android:id="@+id/Back" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="@string/back" /> <CheckBox android:id="@+id/Feed01" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="CheckBox" android:visibility="invisible" /> <CheckBox android:id="@+id/Feed02" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="CheckBox" android:visibility="invisible" /> <CheckBox android:id="@+id/Feed03" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="5dip" android:text="CheckBox" android:visibility="invisible" /> <Button android:id="@+id/AddFeed" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_margin="5dip" android:text="@string/addfeed" android:visibility="invisible" /> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.beatcraft.rssreader" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="7" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:name="com.beatcraft.rssreader.RSSReaderApplication" > <activity android:label="@string/app_name" android:name=".RSSReaderActivity" > <intent-filter > <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:label="@string/app_name" android:name=".ConfigActivity" /> <activity android:label="@string/app_name" android:name=".DescActivity" /> <activity android:label="@string/app_name" android:name=".RecommendActivity" /> </application> <uses-permission android:name="android.permission.INTERNET" > </uses-permission> </manifest>