2010年11月28日日曜日

Google Image Search API の android サンプル・アプリ作ってみた

Google Image Search API を使用した android アプリを作成してみました。

Google Image Search API は以下のサイトにあるように、q=xxx の部分で検索キーワードを指定します。
応答は JSON フォーマットです。


Google Image Search API



検索例: 高知城

リクエスト

レスポンス
{"responseData": {"results":[{"GsearchResultClass":"GimageSearch","width":"600","height":"400","imageId":"Ehuk98rEYswf5M:","tbWidth":"135","tbHeight":"90","unescapedUrl":"http://img01.kitaguni.tv/usr/akkesi1946/1%E9%AB%98%E7%9F%A5%E5%9F%8E.jpg","url":"http://img01.kitaguni.tv/usr/akkesi1946/1%25E9%25AB%2598%25E7%259F%25A5%25E5%259F%258E.jpg","visibleUrl":"akkesi1946.kitaguni.tv","title":"風のふくまま・・・:\u003cb\u003e高知城\u003c/b\u003e","titleNoFormatting":"風のふくまま・・・:高知城","originalContextUrl":"http://akkesi1946.kitaguni.tv/e543472.html","content":"\u003cb\u003e高知城\u003c/b\u003e","contentNoFormatting":"高知城","tbUrl":"http://images.google.com/images?q\u003dtbn:Ehuk98rEYswf5M::img01.kitaguni.tv/usr/akkesi1946/1%25E9%25AB%2598%25E7%259F%25A5%25E5%259F%258E.jpg"},{"GsearchResultClass":"GimageSearch","width":"420","height":"313","imageId":"MHxe0rjkGw3MRM:","tbWidth":"125","tbHeight":"93","unescapedUrl":"http://ozu.cc.kochi-u.ac.jp/~fuchu/syakai/top_img2%5B1%5D.jpg","url":"http://ozu.cc.kochi-u.ac.jp/~fuchu/syakai/top_img2%255B1%255D.jpg","visibleUrl":"ozu.cc.kochi-u.ac.jp","title":"top_img2[1].jpg","titleNoFormatting":"top_img2[1].jpg","originalContextUrl":"http://ozu.cc.kochi-u.ac.jp/~fuchu/syakai/kochicastle.html","content":"\u003cb\u003e高知城\u003c/b\u003e","contentNoFormatting":"高知城","tbUrl":"http://images.google.com/images?q\u003dtbn:MHxe0rjkGw3MRM::ozu.cc.kochi-u.ac.jp/~fuchu/syakai/top_img2%255B1%255D.jpg"},{"GsearchResultClass":"GimageSearch","width":"640","height":"480","imageId":"7H2hzs-xvZA-kM:","tbWidth":"137","tbHeight":"103","unescapedUrl":"http://map.papanavi.jp/ucan/img/sho/39000373_1.jpg","url":"http://map.papanavi.jp/ucan/img/sho/39000373_1.jpg","visibleUrl":"map.papanavi.jp","title":"\u003cb\u003e高知城\u003c/b\u003e | パパナビ","titleNoFormatting":"高知城 | パパナビ","originalContextUrl":"http://map.papanavi.jp/p/spot/details/S39000373-001","content":"\u003cb\u003e高知城\u003c/b\u003e","contentNoFormatting":"高知城","tbUrl":"http://images.google.com/images?q\u003dtbn:7H2hzs-xvZA-kM::map.papanavi.jp/ucan/img/sho/39000373_1.jpg"},{"GsearchResultClass":"GimageSearch","width":"2048","height":"1536","imageId":"6K73uOHjLlHRpM:","tbWidth":"150","tbHeight":"113","unescapedUrl":"http://userdisk.webry.biglobe.ne.jp/008/871/08/N000/000/000/121957888604916210164.JPG","url":"http://userdisk.webry.biglobe.ne.jp/008/871/08/N000/000/000/121957888604916210164.JPG","visibleUrl":"takachin0413.at.webry.info","title":"たかちゃんの独り言/ウェブリブログ","titleNoFormatting":"たかちゃんの独り言/ウェブリブログ","originalContextUrl":"http://takachin0413.at.webry.info/","content":"山内一豊の\u003cb\u003e高知城\u003c/b\u003eです。","contentNoFormatting":"山内一豊の高知城です。","tbUrl":"http://images.google.com/images?q\u003dtbn:6K73uOHjLlHRpM::userdisk.webry.biglobe.ne.jp/008/871/08/N000/000/000/121957888604916210164.JPG"}],"cursor":{"pages":[{"start":"0","label":1},{"start":"4","label":2},{"start":"8","label":3},{"start":"12","label":4},{"start":"16","label":5},{"start":"20","label":6},{"start":"24","label":7},{"start":"28","label":8}],"estimatedResultCount":"392000","currentPageIndex":0,"moreResultsUrl":"http://www.google.com/images?oe\u003dutf8\u0026ie\u003dutf8\u0026source\u003duds\u0026start\u003d0\u0026hl\u003dja\u0026q\u003d%E9%AB%98%E7%9F%A5%E5%9F%8E"}}, "responseDetails": null, "responseStatus": 200}


今回のサンプル・アプリでは、"title" と "unescapedUrl" の値を利用しています。

JSON は階層構造になっているので、こららの値を取得するには、
"responseData" --> "results" と値を辿っていく必要があります。

JSON の解析は、org.json.JSONObject か org.json.JSONArray を使うと簡単に行えます。

"responseData" は値が1つのなので、JSONObject を、
"results" は値が複数(4つ)なので、JSONArray を使用します。

ポイントは、こんなところです。


サンプル・アプリは、3つの要素で構成しています。

  • 検索キーワードを入力する EditText
  • 検索処理を開始する Button
  • 検索結果を表示する ListView


ListView の項目をクリックすると、
別途 Activity を起動し、画像を表示させるようにしています。



メイン Activity のレイアウトです。

main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<EditText
    android:id="@+id/keyword"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="高知城"
    />
<Button
    android:id="@+id/search"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="イメージ検索"
    />
<ListView
    android:id="@+id/list"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_weight="2"
    />
</LinearLayout>


メイン Activity です。

JsonTestActivity.java
package jp.kochi.rtaki;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;

public class JsonTestActivity extends Activity {

  private static final String QUERY_URL = "https://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=";

  private ListView mListView = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Button search = (Button) findViewById(R.id.search);

    search.setOnClickListener(new OnClickListener() {

      @Override
      public void onClick(View v) {
        EditText keyword = (EditText) findViewById(R.id.keyword);
        String key = keyword.getText().toString();

        updateList(key);
      }
    });

    mListView = (ListView) findViewById(R.id.list);

    mListView.setOnItemClickListener(new OnItemClickListener() {

      @Override
      public void onItemClick(AdapterView<?> parent, View view,
          int position, long id) {

        Map map = (Map) parent.getItemAtPosition(position);
        String url = (String) map.get("unescapedUrl");

        Intent intent = new Intent();
        intent.setClass(JsonTestActivity.this, ImageActivity.class);
        intent.putExtra("url", url);
        
        startActivity(intent);
      }
    });
  }

  private void updateList(String key) {

    ArrayList listData = requestImageSearch(key);

    SimpleAdapter adapter = new SimpleAdapter(this, listData,
        android.R.layout.simple_list_item_1, new String[] { "title" },
        new int[] { android.R.id.text1 });

    mListView.setAdapter(adapter);

  }

  private ArrayList requestImageSearch(String key) {
    Map<String, Object> temp;
    ArrayList<Map> listData = new ArrayList<Map>();

    String encodeKey = Uri.encode(key);

    HttpClient client = new DefaultHttpClient();

    String req = QUERY_URL + encodeKey;

    Log.v("JsonTest", "query :" + req);

    HttpUriRequest httpUriReq = new HttpGet(req);

    try {

      HttpResponse res = client.execute(httpUriReq);

      if (res.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
        Toast.makeText(JsonTestActivity.this,
            "StatusCode = " + res.getStatusLine().getStatusCode(),
            Toast.LENGTH_LONG).show();
      } else {

        
        String entity = EntityUtils.toString(res.getEntity());

        JSONObject jsonObj = new JSONObject(entity);

        String responseStatus = jsonObj.getString("responseStatus");
        Log.v("JsonTest", "responseStatus :" + responseStatus);

        if (responseStatus.equals("200") != true) {
          Toast.makeText(JsonTestActivity.this,
              "responseStatus = " + responseStatus,
              Toast.LENGTH_LONG).show();
        } else {

          JSONObject jsonObjData = jsonObj
              .getJSONObject("responseData");

          JSONArray jsonObjResultArray = jsonObjData
              .getJSONArray("results");
          for (int i = 0; i < jsonObjResultArray.length(); i++) {
            JSONObject jsonObjResult = jsonObjResultArray
                .getJSONObject(i);
            Log.v("JsonTest", "title :"
                + jsonObjResult.getString("title"));

            Map map = new HashMap<String, String>();
            map.put("title", jsonObjResult.getString("title"));
            map.put("unescapedUrl", jsonObjResult
                .getString("unescapedUrl"));
            listData.add(map);
          }

        }

      }

    } catch (ClientProtocolException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    } catch (JSONException e) {
      e.printStackTrace();
    }

    return listData;
  }
}


image.xml と ImageActivity.java は、ListView の項目をクリックしたときに、
画像を表示させる Activity です。

image.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<ImageView
    android:id="@+id/image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    />
</LinearLayout>


ImageActivity.java
package jp.kochi.rtaki;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.widget.ImageView;

public class ImageActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.image);
    
    Intent intent = getIntent();
    String url = intent.getStringExtra("url");
    
    Bitmap bitmap = getImage(url);
    
    ImageView image = (ImageView) findViewById(R.id.image);
    image.setImageBitmap(bitmap);
    
  }

  private Bitmap getImage(String url) {
    Bitmap bitmap = null;
    InputStream in = null;
    BufferedOutputStream out = null;
    byte[] buf = new byte[512];
    int bytes;

    try {
      in = new BufferedInputStream(new URL(url).openStream(), 512);

      final ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
      out = new BufferedOutputStream(dataStream, 512);
      while ((bytes = in.read(buf)) > 0) {
        out.write(buf, 0, bytes);
      }

      out.flush();

      final byte[] data = dataStream.toByteArray();
      BitmapFactory.Options options = new BitmapFactory.Options();
      // options.inSampleSize = 1;

      bitmap = BitmapFactory.decodeByteArray(data, 0, data.length,
          options);

    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        if (in != null)
          in.close();
        if (out != null)
          out.close();

      } catch (IOException e) {
        e.printStackTrace();
      }
    }

    return bitmap;
  }
}


マニフェスト・ファイルは、青字の2行を追加しています。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="jp.kochi.rtaki"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".JsonTestActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <activity android:name=".ImageActivity" />

    </application>
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-sdk android:minSdkVersion="4" />

</manifest> 


以上、はじめてマッッシュアップでした~(・o・)

2010年11月27日土曜日

Google API について調べてみた

Google API って、いろいろとあるな~と気づいた今日この頃です。

どんなものか気になります。

ということで、、調べてみました。

大したことは調べてませんが、気になったところを整理します。

  1. android でどうあつかうか?

    処理の要求は、HTTPのGETリクエスト。
    応答のフォーマットは JSON(ジェイソン、JavaScript Object Notation)。
    JSON フォーマットは、org.json.JSONObject.JSONObject で解析可能。


  2. JSONって何?

    <キー>:<値>のペアをカンマ区切りで列記する形式。
    JavaScript のハッシュ記法と同じ。
    これは、JSON の名前の通り、元々は JavaScript で、そのまま利用することを想定しているため。
    値をタグで挟む(ことが多い) XML よりは記述量が少なくなる傾向がある。


  3. 要求/応答のサンプルを見せてくれ。

    Blog Search API を使った例。
    高知城に関するブログを検索する。

    ブラウザで以下の URL を入力する。

    Blog Search - 検索
    https://ajax.googleapis.com/ajax/services/search/blogs?v=1.0&q=%E9%AB%98%E7%9F%A5%E5%9F%8E

    "v=1.0" はバージョン。

    "q=%E9%AB%98%E7%9F%A5%E5%9F%8E" は検索条件。

    %E9%AB%98%E7%9F%A5%E5%9F%8E は 高知城 を JavaScript の encodeURIComponent() と同等の方式でエンコードした値。
    android の場合、android.net.Uri.encode() でエンコードすればよい。


    上記 HTTP リクエストに対する応答は以下の通り。

    Blog Search - 応答
    {"responseData": {"results":[{"GsearchResultClass":"GblogSearch","title":"\u003cb\u003e高知城\u003c/b\u003e: 青春18切符で行く,日本の「城」巡り","titleNoFormatting":"高知城: 青春18切符で行く,日本の「城」巡り","postUrl":"http://oojijisun.seesaa.net/article/170228621.html","content":"\u003cb\u003e高知城\u003c/b\u003e,現存天守閣のある12城から始め、国内の100名城を鑑賞します。日本の歴史、建築土木技術を語る旅です。春休み、夏休み、冬休み、にカメラを持って行きます。","author":"oojijisun","blogUrl":"http://oojijisun.seesaa.net/","publishedDate":"Sun, 21 Nov 2010 14:45:09 -0800"},{"GsearchResultClass":"GblogSearch","title":"12月の日程予定 - \u003cb\u003e高知城\u003c/b\u003eサッカークラブ情報ブログ","titleNoFormatting":"12月の日程予定 - 高知城サッカークラブ情報ブログ","postUrl":"http://blog.goo.ne.jp/kochijyo-sc/e/8267635293edf7372e58db52ba4917f4","content":"1991年結成 \u003cb\u003e高知\u003c/b\u003e県\u003cb\u003e高知\u003c/b\u003e市で活動する壮年サッカーチームの情報交換広場です.","author":"kochijyo-sc","blogUrl":"http://blog.goo.ne.jp/kochijyo-sc/","publishedDate":"Wed, 24 Nov 2010 01:07:38 -0800"},{"GsearchResultClass":"GblogSearch","title":"山内一豊と千代さんの夫婦愛の結晶 \u003cb\u003e高知城\u003c/b\u003eを歩こう!","titleNoFormatting":"山内一豊と千代さんの夫婦愛の結晶 高知城を歩こう!","postUrl":"http://www.pippikochi.or.jp/events/2010/12/post-252.shtml","content":"さすが敵から身を守るお城!\u003cb\u003e高知城\u003c/b\u003eの至る所に仕掛けがあるのをご存知ですか?知られていない\u003cb\u003e高知城\u003c/b\u003eの姿がわかるかも!?私たちと一緒に歩いてみませんか。 【日時】12月4日(土)13:30~15:30 【集合場所】高知樹追手門前 【定員】20名(先着) ...","author":"kvnc2","blogUrl":"http://www.pippikochi.or.jp/events/","publishedDate":"Wed, 24 Nov 2010 16:58:46 -0800"},{"GsearchResultClass":"GblogSearch","title":"中央公園と\u003cb\u003e高知城\u003c/b\u003e - こんぶろ-酒屋のブログ-","titleNoFormatting":"中央公園と高知城 - こんぶろ-酒屋のブログ-","postUrl":"http://blog.goo.ne.jp/konjiru/e/584250c87cdbc6c0b25926c2406b68af","content":"とある小さな酒屋、近藤印 \u003cb\u003e高知\u003c/b\u003e酒店のお酒にまつわったり・まつわんなかったりするブログです。","author":"konjiru","blogUrl":"http://blog.goo.ne.jp/konjiru/","publishedDate":"Tue, 16 Nov 2010 06:28:49 -0800"}],"cursor":{"pages":[{"start":"0","label":1},{"start":"4","label":2},{"start":"8","label":3},{"start":"12","label":4},{"start":"16","label":5},{"start":"20","label":6},{"start":"24","label":7},{"start":"28","label":8}],"estimatedResultCount":"80969","currentPageIndex":0,"moreResultsUrl":"http://blogsearch.google.com/blogsearch?oe\u003dutf8\u0026ie\u003dutf8\u0026safe\u003dactive\u0026source\u003duds\u0026start\u003d0\u0026hl\u003dja\u0026q\u003d%E9%AB%98%E7%9F%A5%E5%9F%8E"}}, "responseDetails": null, "responseStatus": 200}


    ブラウザ画面



  4. Google API には、どんなものがある?

    以下のサイトの APIs Using the Loader にあるものが Google API。
    但し、JSON に対応していないものがある。

    http://code.google.com/intl/ja/apis/loader/index.html


実際に、Google API を使った android のサンプル・アプリを作ってみようと思います~
たぶん。

2010年11月15日月曜日

お天気APIを探してみた

マッシュアップに利用できるお天気APIを探してみました。

検索条件は、フリーで、日本以外の天気も検索できること、です。

2つ見つけたので、メモしておきますが、
いずれも使用には制限(難点)があります。。


Google の隠しAPI
  1. 使用例

    福岡
    http://www.google.com/ig/api?weather=Fukuoka

    ロンドン
    http://www.google.com/ig/api?weather=London

    ニューヨーク
    http://www.google.com/ig/api?weather=new+york


  2. 備考

    後述の Yahoo(米国)のAPI と違って、地方(高知とか徳島とか)も取得できます。

    緯度/経度による検索も可能です。(緯度/経度は 10 の 6 乗した整数値に変換)

    緯度/経度による検索例 (福岡:緯度=33.35 経度=130.24 の場合)
    http://www.google.com/ig/api?weather=,,,33350000,130240000


  3. 問題点

    機能的には申し分ないのですが、Google非公開なので、いつなくなるか分かりません。
    Google非公開なので商用利用の可否も不明です。
    賭けで使用するかしないかは、アナタ次第です!(某やりすぎより引用)

Yahoo(米国)のAPI
  1. 使用例

    福岡
    http://weather.yahooapis.com/forecastrss?w=1117099

    ロンドン
    http://weather.yahooapis.com/forecastrss?w=44418

    ニューヨーク
    http://weather.yahooapis.com/forecastrss?w=2459115


  2. 備考

    w=xxxxxxx の部分は、http://weather.yahoo.com/ にて、
    該当都市の検索後に、URL の末尾に表示されている値です。

    福岡の場合は以下の画像のように、
    http://weather.yahoo.com/japan/fukuoka-prefecture/fukuoka-shi-1117099/
    となるので、1117099 が w=xxxxx で指定する値になります。



  3. 問題点

    商用利用は不可。
    http://developer.yahoo.com/weather/ に "non-commercial uses" との記載あり。

    少なくとも日本の場合、主要都市しか情報が取得できない。
    高知県と徳島県はない。愛媛県と香川県はあるのに。。

2010年11月11日木曜日

subclipse で bin フォルダをバージョン管理から除外する方法

subclipse で android プロジェクトの bin フォルダをバージョン管理から除外する方法が分かったので、
メモしておきます。

  1. 切断&リポジトリー削除

    一旦、バージョン管理から除外するために、切断&リポジトリー削除を行います。

    これから新規にリポジトリー登録を行う場合は、この手順はスキップします。


    まずは、切断。

    Eclipse の Package Explorer より、該当のプロジェクトを右クリックし、
    "Team" --> "切断..." を選択します。


    "SVNから切断を確認" のダイアログが表示されたら、
    "ファイル・システムから SVN メタ情報も削除します。" を選択し、"Yes" を押します。


    次は、リポジトリーの削除。

    SVN リポジトリー・エクスプローラーより、該当のプロジェクトを右クリックし、
    "削除..." を選択します。


    "コミット" のダイアログが表示されたら、
    適当にコメントを編集(しなくてもいいですが)して、"OK" を押します。


  2. SVN リポジトリー登録

    Package Explorer から、SVN リポジトリーに登録するプロジェクトを右クリックし、
    "Team" --> "Share Project..." を選択します。


    "Share Project" のダイアログが表示されたら、
    "SVN" を選択し、通常の手順で、リポジトリー登録を行います。

    この時点では、SVN リポジトリー側はフォルダが作成されただけで、
    ファイルのインポートは実施されていません。
    (SVN リポジトリー・エクスプローラーで確認可能)

    ここがポイントですが、ファイルの初回インポートを行う前に、
    bin フォルダについて、svn:ignore 属性をセットする必要があります。

    で、その方法ですが、
    Package Explorer で該当のプロジェクトを右クリックし、
    "Team" --> "リポジトリーと同期化" を選択します。


    Team Synchroning が表示されるので、
    bin フォルダを右クリックし、"svn:ignore に追加" を選択します。


    "カスタム・パターン" で "bin" (デフォルト) を指定し、"OK" を押します。

    これで、bin フォルダはバージョン管理対象外となりますので、
    後は通常通り、コミットをすればよいです。

2010年11月10日水曜日

Subversion リポジトリのバックアップ (svnsync編)

Subversion のリポジトリについて、バックアップ手順をメモ。

まずは、svnsync の場合。

svnsync は、別の PC に Subversion のリポジトリを作成し、複製するというもの。

  1. 複製用リポジトリ作成

    C:\> mkdir C:\subversion

    C:\> cd C:\subversion

    C:\subversion> svnadmin create backup


  2. pre-revprop-change.bat 作成
    C:\subversion\backup\hooks\pre-revprop-change.bat を新規に作成し、
    exit /b 0 の1行のみを記述。

    これを実施しないと、svnsync sync 実行時に以下のエラーが出力される。

    svnsync: リポジトリが、リビジョン属性を変更できるようにはなっていません。
    管理者に pre-revprop-change フックを作成するよう頼んでください


  3. 同期を実施

    C:\subversion> svnsync init file:///subversion/backup https://www.xxx.yyy.zzz:8080/svn/svn_project
    リビジョン 0 の属性をコピーしました。

    C:\subversion> svnsync sync file:///subversion/backup
    リビジョン 1 をコミットしました。
    リビジョン 1 の属性をコピーしました。
    リビジョン 2 をコミットしました。
    リビジョン 2 の属性をコピーしました。
    リビジョン 3 をコミットしました。
    (中略)
    ファイルのデータを送信しています ..
    リビジョン 113 をコミットしました。
    リビジョン 113 の属性をコピーしました。
    C:\subversion>

    ちなみに proxy 経由で、複製元(http(s)://~)に接続する場合は、
    servers ファイル(*1)に proxy 設定の記述が必要。

    (*1) Windows XP の場合。
           C:\Documents and Settings\<ユーザー名>\Application Data\Subversion\servers

ん~。
ちなみに複製元(http(s)://~)には、Basic 認証でユーザー名/パスワードを入力する必要があるのですが、
svnsync init 及び sync ともに、ユーザー名/パスワードを指定していません。

何で指定しなくても処理できるのか、薄気味悪かったのですが、
調べてみると、以下のフォルダにあるファイルにて、ユーザー名/パスワードを記録しているようです。

C:\Documents and Settings\<ユーザー名>\Application Data\Subversion\auth\svn.simple

確かに、上記フォルダを見ると、
16進数(0~9, a~f)で構成された 30 文字くらいのファイルがあり、
そのファイルの中身を見ると、パスワードは暗号化されているようで分かりませんでしたが、
(当たり前と言えば当たり前)、ユーザー名が記述されていました。

AdMob に挑戦!

AdMob とは、モバイル向けの広告サービス(Google AdSense のモバイル版)です。

ということを本日知りました(^_^;)アセアセ

・・・ということで早速試してみることにしました!



しか~し!
当初、以下のサイトを見ていたのですが、どうも情報が古いようで。。

http://developer.admob.com/wiki/Android

上記サイト通りにやると、以下のようなエラーが LogCat に記録されて、
ちっとも広告が表示されませんでした。シクシク。

could not find com.admob.android.ads.AdMobActivity, please make sure it is registered in AndroidManifest.xml
com.admob.android.ads.AdMobActivity must be registered in your AndroidManifest.xml file.


ということで、、

以下のPDFを参考にして作業をすれば、無事、広告表示までサンプル・アプリを作成できました。
参考PDF:
http://www.admob.com/docs/AdMob_Android_SDK_Instructions.pdf


以下に、手順をメモしておきます。

  1. AdMob のアカウントを作成。

    AdMob サイト より、"未登録の方は、今すぐ登録する" を選択します。

    すると、アカウント情報を入力する画面になるので、太字になっている箇所を入力します。

    入力が終わると、アカウント情報で指定したメールアドレスにメールが配信されます。
    配信されたメールに、アカウントを有効化にするリンクがあるので、このリンクをクリックします。

    以上で、アカウント作成は完了です。


  2. AdMob SDK 入手

    作成したアカウントでログインします。

    ログイン後、"サイト及びアプリケーション" のタブを選択し、
    "+サイト/アプリケーションの追加" をクリックします。



    アプリケーションのタイプとして、"Android アプリケーション" を選択します。
    App名、ジャンル、Appの説明の 3 つは入力が必須なので、適当に入力します。
    Android パッケージURL は必須ではないので、特になければ空白にしておきます。



     アプリケーションの登録が完了後、
    "AdMob Android SDK のダウンロード" をクリックし、SDK をダウンロードします。



  3. AdMob サンプル・アプリの作成 (プロジェクト作成~ライブラリ追加)

    android プロジェクトを作成します。
    とりあえず、以下の感じで作成してみました。

    項目
    Project name AdMobTest Proj
    Build Target Android 1.6
    Application name   AdMobTest
    Package name jp.kochi.AdMobTest   
    Create Activity AdMobTestActivity
    Min SDK Version 4


    プロジェクトを作成したら、libs という名前でフォルダを作成します。

    libs フォルダを作成したら、その中に、admob-sdk-android.jar をコピー(Import)します。

    次に、プロジェクトの Properties を開き、"Java Build Path" を選択し、
    "Libraries" から "Add JARs..." を実行して、libs フォルダにコピーした admob-sdk-android.jar を選びます。




  4. AdMob サンプル・アプリの作成 (パブリッシャーIDの登録~アプリ完成/起動確認)

    マニフェスト・ファイルに修正(青字部分が追記部分)します。

    AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="jp.kochi.AdMobTest" android:versionCode="1"
      android:versionName="1.0">
      <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".AdMobTestActivity" android:label="@string/app_name">
          <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
          </intent-filter>
        </activity>

        <!-- The application's publisher ID assigned by AdMob -->
        <meta-data android:value="a14xxxxxxxxxxxx" android:name="ADMOB_PUBLISHER_ID" />

        <!-- AdMobActivity definition -->
        <activity android:name="com.admob.android.ads.AdMobActivity"
          android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
          android:configChanges="orientation|keyboard|keyboardHidden" />
        <!-- Track Market installs -->
        <receiver android:name="com.admob.android.ads.analytics.InstallReceiver"
          android:exported="true">
          <intent-filter>
            <action android:name="com.android.vending.INSTALL_REFERRER" />
          </intent-filter>
        </receiver>

        <meta-data android:value="true" android:name="ADMOB_ALLOW_LOCATION_FOR_ADS" />

      </application>
      <uses-sdk android:minSdkVersion="4" />

      <uses-permission android:name="android.permission.INTERNET" />
      <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
      <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
    </manifest> 

    meta-data android:value="a14xxxxxxxxxxxx" で指定するパブリッシャーIDは、
    AdMob のサイトにて、"サイト及びアプリケーション" から該当アプリの "設定管理" より確認できます。



    res/values フォルダに、attr.xml を新規作成し、以下の内容を記述します。

    res/values/attr.xml
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
      <declare-styleable name="com.admob.android.ads.AdView">
        <attr name="backgroundColor" format="color" />
        <attr name="primaryTextColor" format="color" />
        <attr name="secondaryTextColor" format="color" />
        <attr name="keywords" format="string" />
        <attr name="refreshInterval" format="integer" />
      </declare-styleable>
    </resources>


    res/layout/main.xml に以下の内容を記述します。
    xmlns:myapp="http://schemas.android.com/apk/res/<パッケージ名>" の <パッケージ名> には、
    マニフェスト・ファイルに記述されているパッケージ名を指定します。

    res/layout/main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:myapp="http://schemas.android.com/apk/res/jp.kochi.AdMobTest"
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent">
      <com.admob.android.ads.AdView
        android:id="@+id/ad"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        myapp:backgroundColor="#000000"
        myapp:primaryTextColor="#FFFFFF"
        myapp:secondaryTextColor="#CCCCCC" />
    </LinearLayout>


    最後に Activity です。
    エミューレーターで動作確認をする場合、
    以下のように、テストモードに設定する必要があるようです。

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

            AdManager.setTestDevices(new String[] { AdManager.TEST_EMULATOR });

            AdView adView = (AdView)findViewById(R.id.ad);
            adView.requestFreshAd();

        }
    }

    以上でサンプル・アプリ作成は完了です。

    ちなみに、実機で稼動させると、LogCat 上に以下のようなメッセージ(デバイスID)が表示されます。

    I/AdMobSDK( 1712): To get test ads on this device use AdManager.setTestDevices(new String[] { "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } )

    エミュレータに加え、実機でもテスト・モードで稼動させるには、
    この xxxxx のデバイスIDも、AdManager.setTestDevices() で指定すればよいです。

    AdManager.setTestDevices(new String[] { AdManager.TEST_EMULATOR, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" });


    エミュレータで稼動させると、1回目はうまく表示されませんでしたが、
    2回目は、きちんと広告が表示されました~


何だか、最後まで、すんなりといきませんでしたが、
とりあえずヨシとしておきます。

2010年11月8日月曜日

BroadcastReceiver を別プロセスで動かしてみたときの感想文

BroadcastReceiver を別プロセスで動かしたところ、
ただの知識不足が原因と言えるのですが、
一部、予想外の動きだったので、その驚き?をメモしておきます。

別プロセスで動かすということで、
マニフェスト・ファイルには、以下のように記述しました。

AndroidManifest.xml
<receiver android:name=".LocationReceiver" android:process=":remote"/>

LocationReceiver が BroadcastReceiver を拡張したクラスになります。


LocationReceiver には以下のような記述をしました。

LocationReceiver.java
public class LocationReceiver extends BroadcastReceiver {

  LocationManager mLocationMgr;
  
  public void onReceive(Context arg0, Intent arg1) {

    if (mLocationMgr == null) {
      mLocationMgr = (LocationManager) arg0
        .getSystemService(arg0.LOCATION_SERVICE);
    }
  }
  
}

で、ここから意外だったんですが、
毎回毎回 mLocationMgr が null になることが分かりました。

2回目以降の呼び出しでは、null 以外を想定していました。


なんでじゃい!と思って、調べてみると、

まず、BroadcastReceiver は onReceive() を実行している間のみ Active で、
実行が終われば、Inactive になると記述があります。

Broadcast receiver lifecycle からの抜粋。
When a broadcast message arrives for the receiver, Android calls its onReceive() method and passes it the Intent object containing the message. The broadcast receiver is considered to be active only while it is executing this method. When onReceive() returns, it is inactive.

さらに同ページのプロセスのライフ・サイクルの記述を見ると、
Empty process という存在の記述があります。

Processes and lifecycles
  1. An empty process is one that doesn't hold any active application components. The only reason to keep such a process around is as a cache to improve startup time the next time a component needs to run in it. The system often kills these processes in order to balance overall system resources between process caches and the underlying kernel caches.

DDMS で見ると、":remote" がプロセス名に付与されたプロセスが同一の PID で存在し続けるので、
onReceive() 実行後も empty process として残っていると考えられます。

以上の2点を踏まえて、今回の動きを説明すると、
どうも以下のような動きをしているようです。
  1. プロセス生成。
  2. LocationReceiver 初期化、onReceiver() 実行。
  3. onReceiver() 終了後、LocationReceiver を破棄。
  4. プロセスは残る。

で、2回目以降の呼び出しでは、2. から実行される、と思われます。

ふ~ん、、、って感じですね。。
( ̄ー ̄;アセアセ

2010年11月4日木曜日

gapps-mdpi-tiny-20101020-signed.zip を適用してみた

いまさらですが。。

CyanogenMod 6.0.0 に更新してから、2ヶ月以上経過して、
GPS が機能していないことに気付きました。

う~ん。いろいろと、いじっているうちに、おかしくなったのかもしれませんが、
最初から、こうだったのかは、もう分かりません Y(>_<、)Y ヒェェ!

HT-03A を CyanogenMod 6.0.0 に更新する際に、
以下の2ファイルを適用したわけですが、Google Addon については、
さらに新しいバージョンが公開されていたので、適用したら、GPS 使えました!


9月15日に更新したファイル

update-cm-6.0.0-DS-signed.zip
gapps-mdpi-tiny-20100816-signed.zip


以下に手順をメモしておきます。

[注意]
  手順の中で Wipe を実行することで、導入したアプリの削除、
  各種設定の初期化を実施しています。

  1. RA-sapphire を起動 (HOME+電源キー同時押し)

  2. バックアップ取得。
    Backup/Restore --> Nand backup

  3. Google Addon をダウンロード。

    サイト:
      http://wiki.cyanogenmod.com/index.php?title=Latest_Version

    ファイル:
      gapps-mdpi-tiny-20101020-signed.zip

  4. SDカードにファイルを配置。

  5. PCとHT-03Aを結ぶUSBケーブルを外す。

  6. RA-sapphire を起動 (HOME+電源キー同時押し)

  7. Wipe 実行。
    Wipre --> Wipe data/factory reset

  8. gapps-mdpi-tiny-20100816-signed.zip 適用。
    Flash zip from sdcard -->SDCARD:gapps-mdpi-tiny-20101020-signed.zip

  9. 起動。
    Reboot system now

無事、Google Map で GPS 機能が動いていることが確認できました v(。・ω・。)ィェィ♪

しかし、アプリの再導入やら、Google アカウントの初期設定やら、
全てやり直しですけど。。Σ(T▽T;) ぐわわぁぁ~ん!

2010年11月1日月曜日

MapView でアニメーション (Overlay編)

MapView を使って、Google マップを表示させている上に、
画像をアニメーションさせるサンプル・プログラムの2つ目です。

2つ目は、日本Androidの会のメーリング・リストでやりとりされていた
以下のスレッドを参考にさせて頂きました。
参考サイト: OverlayにImageViewを表示する方法について

参考サイトでは、OverlayItem を使用しているのですが、
表示させたい画像(アニメーション)は1つしかないので、Overlay を使用するようにしました。


<設計方針>

Overlay を使います。

Overlay の draw() で画像を表示させるわけですが、画像を60枚用意しておき、
draw() が実行される都度、画像を切り替えるようにしておきます。
(最後の画像になったら、最初の画像に戻ります)

MapView に対して定期的に invalidate() を実行することで、Overlay の draw() が定期的に
実行されるようになり、パラパラ漫画(アニメーション)の完成です。

<問題点>

問題点でないですが、MapView に対して invalidate() を実行しているため、
複数の画像(静止画)があった場合、それらも再描画対象となるかなぁというのが気にかかります。


以下にポイントとなるソースを貼り付けます。

まずはレイアウトです。
FrameLayout に対し、MapView のみを表示させています。
(子要素が MapView しかないので、FrameLayout である必要はないんですけど。)

main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<com.google.android.maps.MapView
 android:id="@+id/mapview"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:enabled="true"
 android:clickable="true"
 android:apiKey="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
 />
</FrameLayout>


次は Activity (MapActivity継承) クラスです。

onCreate() では、person_01.png ~ person_60.png の 60 枚の画像を読み込んでいます。
その後、アニメーションを行う Overlay を MapView に紐付けています。

MapTestActivity.java
package jp.kochi.test;

import java.util.ArrayList;
import java.util.List;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.AnimationDrawable;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;

public class MapTestActivity extends MapActivity {
 ImageView mImageView;
 AnimationOverlay mAnimOverlay;

 @Override
 public void onCreate(Bundle savedInstanceState) {
  Log.v("MapTest", "onCreate()");
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  MapView mapView = (MapView) findViewById(R.id.mapview);

  mapView.setBuiltInZoomControls(true);

  ArrayList<Bitmap> list = new ArrayList<Bitmap>();
  Resources r = getResources();

  int id;
  Bitmap bmp;
  String str;
  for (int i = 1; i <= 60; i++) {
   str = "person_" + String.format("%1$02d", i);
   id = r.getIdentifier(str, "drawable", getPackageName());
   Log.v("MapTest", "str = " + str + ", id = " + id);
   bmp = BitmapFactory.decodeResource(r, id);
   list.add(bmp);
  }

  Log.v("MapTest", "list.size() = " + list.size());

  // アニメーション表示
  // お遍路さん2番札所
  double latitude  = 34.156028;
  double longitude = 134.49025;
  GeoPoint gpoint = new GeoPoint((int)(latitude * 1E6), (int)(longitude * 1E6));
  List listOverlays = mapView.getOverlays();
  mAnimOverlay = new AnimationOverlay(gpoint, list, mapView);
  listOverlays.add(mAnimOverlay);

 }
 
 @Override
 protected void onResume() {
  Log.v("MapTest", "onResume()");
  super.onResume();
  mAnimOverlay.resume();
 }
 
 @Override
 protected void onPause() {
  Log.v("MapTest", "onPause()");
  super.onPause();
  mAnimOverlay.pause();
 }

 @Override
 protected boolean isRouteDisplayed() {
  // TODO Auto-generated method stub
  return false;
 }
}

onResume()、onPause() では、Overlay のアニメーション (定期的に invalidate() 実行) を
再開/停止させる処理を追加しています。


最後は AnimationOverlay (Overlay継承) クラスです。

AnimationOverlay.java
package jp.kochi.test;

import java.util.ArrayList;
import java.util.List;
import android.R.bool;
import android.R.drawable;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.Log;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;

public class AnimationOverlay extends Overlay implements Runnable {
 private Handler mHandler;
 private ArrayList<Bitmap> mPngList;
 private int current_frame;
 private MapView mMapView;
 private GeoPoint mGeoPoint;
 final private int sInterval = 100;
 private boolean mStop = false;

 public AnimationOverlay(GeoPoint point, ArrayList<Bitmap> list,
   MapView mview) {
  Log.v("MapTest", "AnimationOverlay()");

  mPngList = list;
  mMapView = mview;
  mGeoPoint = point;

  current_frame = 0;

  mHandler = new Handler();
  mHandler.postDelayed(this, sInterval);
 }

 @Override
 public void draw(Canvas canvas, MapView mapView, boolean shadow) {
  super.draw(canvas, mapView, shadow);

  if (shadow == false) {
   Log.v("MapTest", "draw() : shadow = false");
   Point pxPoint = new Point();

   Projection projection = mMapView.getProjection();
   projection.toPixels(mGeoPoint, pxPoint);

   canvas.drawBitmap(mPngList.get(current_frame), pxPoint.x,
     pxPoint.y, null);
  } else {
   Log.v("MapTest", "draw() : shadow = true");
  }
 }

 @Override
 public void run() {
  Log.v("MapTest", "run()");
  if (current_frame == mPngList.size() - 1)
   current_frame = 0;
  else
   current_frame++;

  mMapView.invalidate();

  if (mStop == false) mHandler.postDelayed(this, sInterval);
  
 }
 
 public void resume() {
  mHandler.postDelayed(this, sInterval);
  mStop = false;
 }
 
 public void pause() {
  mStop = true;
 }
 
}


以下は実行画面です。
これもまた、分かりにくいですが、画面中央付近の画像が変化(アニメーション)しています。