2010年12月30日木曜日

Resource is not public

システムが持つリソース・ファイル (各種 XML ファイルや PNG ファイルなど) は参照できるものと参照できないものがあるようです。

参照できない場合、Eclipse のコンソールに以下のようなエラーが出力されます。

[2010-12-30 11:15:19 - XXXXXXX Proj] C:\android\workspace\XXXXXXX Proj\res\values\styles.xml:77: error: Error: Resource is not public. (at 'android:background' with value '@android:drawable/btn_circle').

@android:drawable/btn_circle を指定して、
<android SDK導入ディレクトリ>\platforms\android-X\data\res\drawable\btn_circle.xml (android-X の X は API レベル) を参照しようとしているのですが、"public じゃないよ" と叱られています。

public かどうかは、その名の通り public.xml で確認できます。
(android 2.2 の場合)
<android SDK導入ディレクトリ>\platforms\android-8\data\res\values\public.xml

ちなみに、public でなくとも、@*android:drawable/btn_circle のように、@ の後ろに * (アスタリスク) を入れると参照できるようになります。

但し、以下のメーリング・リストのやりとりを見ると、public でないものは、頻繁に変更が入るみたいなので、基本的には使用しないほうが無難なようです。
what star (*) means in "@*android:drawable/"?
https://groups.google.com/group/android-developers/browse_thread/thread/d1e06ce82e1f745b?fwc=1&hl=ja

2010年12月24日金曜日

java.lang.IllegalStateException: Immutable bitmap passed to Canvas constructor

png などの画像上に drawText() で文字を書こうとして、
以下のような処理を記述すると、4行目の drawText() で、タイトルの例外メッセージが発生します。

Resources r = getResources();
Bitmap bitmap = BitmapFactory.decodeResource(r, R.drawable.icon);
Canvas canvas = new Canvas(bitmap);
canvas.drawText(...);

bitmap が immutable (変えられない) ことが原因です。
mutable か immutable かは、bitmap.isMutable() で true か false かで確認できます。

じゃあ、どうやって mutable にするかというと、
bitmap.copy(Bitmap.Config config, boolean isMutable) を使って、
mutable な Bitmap を新規に作成します。

青字部分が修正内容)
Resources r = getResources();
Bitmap bitmap = BitmapFactory.decodeResource(r, R.drawable.icon);
Bitmap copy = bitmap.copy(Bitmap.Config.ARGB_8888, true);
Canvas canvas = new Canvas(copy);
canvas.drawText(...);

こうすると、うまく処理できます。


ちなみに、、

bitmap.copy(Bitmap.Config config, boolean isMutable) の第1引数ですが、
ALPHA_8、ARGB_4444、ARGB_8888、RGB_565 のいずれかを選択できます。

ARGB_4444 や RGB_565 を使えば、画像のバイト数を節約できそうです。
画質優先の場合には、ARGB_8888 を使う、というところでしょうか。
ALPHA_8 は、よく分かりません。。

項目 1 ピクセルあたりのバイト数 コピー内容(多分。確証はありません。)
ALPHA_8 1 バイト アルファ値のみ 8 ビット。
ARGB_4444  2 バイト  ARGB それぞれ 4 ビットずつ。
ARGB_8888 4 バイト ARGB それぞれ 8 ビットずつ。
RGB_565 2 バイト RGB が 5/6/5 ビット。


以下は、比較用に作成したサンプルです。


上から、以下の画像を表示させています。
  1. オリジナル画像
  2. ALPHA_8 (表示されていませんが。。)
  3. ARGB_4444
  4. ARGB_8888
  5. RGB_565

以下は、ソースです。

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"
  >
<ImageView
  android:id="@+id/imageview1"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  />
<ImageView
  android:id="@+id/imageview2"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  />
<ImageView
  android:id="@+id/imageview3"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  />
<ImageView
  android:id="@+id/imageview4"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  />
<ImageView
  android:id="@+id/imageview5"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  />
</LinearLayout>

BitmapCopyActivity.java
package jp.sample.rtaki;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.widget.ImageView;

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

    Paint paint = new Paint();
    paint.setColor(Color.BLUE);
    //paint.setAlpha(200);
    paint.setTextSize(12);

    ImageView v1= (ImageView) findViewById(R.id.imageview1);
    Bitmap image = BitmapFactory.decodeResource(getResources(), R.drawable.flag);
    v1.setImageBitmap(image);

    ImageView v2 = (ImageView) findViewById(R.id.imageview2);
    Bitmap copy1 = image.copy(Bitmap.Config.ALPHA_8, true);
    Canvas c2 = new Canvas(copy1);
    c2.drawText("ALPHA_8 - " + copy1.getRowBytes()/copy1.getWidth()*1.0, 25, 33, paint);
    v2.setImageBitmap(copy1);

    ImageView v3 = (ImageView) findViewById(R.id.imageview3);
    Bitmap copy2 = image.copy(Bitmap.Config.ARGB_4444, true);
    Canvas c3 = new Canvas(copy2);
    c3.drawText("ARGB_4444 - " + copy2.getRowBytes()/copy2.getWidth()*1.0, 25, 33, paint);
    v3.setImageBitmap(copy2);

    ImageView v4 = (ImageView) findViewById(R.id.imageview4);
    Bitmap copy3 = image.copy(Bitmap.Config.ARGB_8888, true);
    Canvas c4 = new Canvas(copy3);
    c4.drawText("ARGB_8888 - " + copy3.getRowBytes()/copy3.getWidth()*1.0, 25, 33, paint);
    v4.setImageBitmap(copy3);

    ImageView v5 = (ImageView) findViewById(R.id.imageview5);
    Bitmap copy4 = image.copy(Bitmap.Config.RGB_565, true);
    Canvas c5 = new Canvas(copy4);
    c5.drawText("RGB_565 - " + copy4.getRowBytes()/copy4.getWidth()*1.0, 25, 33, paint);
    v5.setImageBitmap(copy4);

  }

}

2010年12月17日金曜日

電波改善計画の電話がきた

android とは、ちっとも関係ありませんが、、

本日、ソフトバンクのコールセンター?の おば じゃなくて、おねえさんから、
電話改善計画のアンケートなる電話がありました。

電話番号は 0120-669-600 だったんですが、
ググると結構ヒットしたので、既に多くの人に電話しているようです。

お小遣い的都合で、android への機種変更ができず、
iPhone 3GS を使い続けているのですが、以下のようなことを質問されました。

  • 自宅では、iPhone を Wi-Fi で使っているか?

  • 自宅のインターネット環境は光か、それともADSLか?

  • プロバイダはどこか?

  • Wi-Fi ルータのメーカーは何か?家電量販店で買ったものか?

  • iPhone でインターネット接続をよくする時間帯はいつか?

  • 自宅で Wi-Fi 使用をどのように確認しているか?
    ちゃんと画面上部のアイコンが 3G から 扇型 になることを確認しているか?

こっちは仕事中なのに、いきなり電話を掛けてきて、
何を聞くんだ?と、ちょっとイラっとしましたが、
この おば じゃなくて、おねえさんに、そんなことを言っても仕方がないので、
いつもの事なかれ主義で、淡々と質問に答えました。

いきなり電話して、こんな質問しようもんなら、相手は100%嫌がると思います。

そう考えると、むしろ、おねえさんが不憫です。。
派遣の自給はいくらですか。。自分より給料よかったりして。

最後に、電話が切られそうになったところで、
「質問の主旨は何ですか?」と訊いてみると、以下のような回答でした。

電話改善運動の一貫で、ご自宅での iPhone の使用状況を確認し、
Wi-Fi を使っていないユーザー様については、FON ルータを配布している。


ということは、正直に「Wi-Fi使っています」と答えなければ、
FON ルータを無料で貸し出せてもらえたのかもしれません。。

そう思うと、ちょっと失敗したかな・・・


しかし、よくよく考えて見ると、
この手の電話は、パケットの通信量が多い人を優先もしくは限定しているじゃないでしょうか。

う~ん、最近は、あんまし使ってないんだけどな~

夏頃は、確かにパケット代が 100 万超えてけど。。(もちろん実際は払った金額は定額料金のみですが)

2010年12月8日水曜日

ブログのタイトル、ちょっと変えました

google で検索すると、rtaki さんが既にいらっしゃるようだったので、rtaki0329 にしました。

あんまし、まぎらわしさが解消されていない気もするんですが、
まあ気にする人もいないだろうということで、ヨシとしておきます。

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;
 }
 
}


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

2010年10月29日金曜日

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

MapView を使って、Google マップを表示させている上に、
画像をアニメーションさせるサンプル・プログラムを2パターン作成してみました。

まずは1つ目。

<設計方針>

画面のレイアウトは、FrameLayout にして、
FrameLayout 一杯に MapView と ImageView を順番に表示します。
そして、ImageView を AnimationDrawable でアニメーションさせます。

<問題点>

画像を表示させる位置が、Google Map 上の位置 (GeoPoint) に対応させることができません。
(というか分かりません。ひょっとしたら何らかの方法があるのかもしれません。。)

そのため、画面の左上のすみっこ等、Google Map の座標(緯度/経度)とは関係なく表示させればよい、
というときくらいには使えるかな~という方法です。


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

まずはレイアウトです。
FrameLayout に対し、MapView、ImageView の順で表示させます。

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"
 />

<ImageView
 android:id="@+id/imageview"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 />

</FrameLayout>


次は、AnimationDrawable に読み込ませるアニメーション・リストです。
drawable フォルダには、person_01.png ~ person_60.png まで 60 の画像ファイルを配置しています。

animation.xml
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
 android:oneshot="false">
 <item android:drawable="@drawable/person_01" android:duration="50" />
 <item android:drawable="@drawable/person_02" android:duration="50" />
 <item android:drawable="@drawable/person_03" android:duration="50" />
 <item android:drawable="@drawable/person_04" android:duration="50" />

  (途中省略)

 <item android:drawable="@drawable/person_58" android:duration="50" />
 <item android:drawable="@drawable/person_59" android:duration="50" />
 <item android:drawable="@drawable/person_60" android:duration="50" />

</animation-list>


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

MapTestActivity.java
package jp.kochi.test;

import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
import android.graphics.drawable.AnimationDrawable;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;

public class MapTestActivity extends MapActivity {
 /** Called when the activity is first created. */
 AnimationDrawable mAnimationDrawable;
 ImageView mImageView;

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

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

  mapView.setBuiltInZoomControls(true);

  mImageView = (ImageView) findViewById(R.id.imageview);
  mImageView.setBackgroundResource(R.anim.animation);
  mAnimationDrawable = (AnimationDrawable) mImageView.getBackground();

 }

 @Override
 public void onWindowFocusChanged(boolean hasFocus) {
  super.onWindowFocusChanged(hasFocus);

  mAnimationDrawable.start();
 }

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


以下は実行画面です。
分かりにくいですが、左上の画像が変化(アニメーション)しています。


エミュレーターで Google マップ表示 (Proxy経由の場合)

Proxy 環境化の PC にて emulator を起動し、Google Map を表示させると、
地図が表示されずに、LogCat 上に以下のエラーが記録されます。

ERROR/ActivityThread(200): Failed to find provider info for com.google.settings
ERROR/MapActivity(222): Couldn't get connection factory client

emulator に対し、Proxy 設定を行えば、解決したのですが、
ググって調べた結果、対応方法が3通りあることが分かったので、以下に整理します。

設定する値は共通です。
設定値
-http-proxy http://<proxyのFQDN又はIPアドレス>:<ポート番号>

emulator の proxy 設定方法。
  1. Default emulator options を設定。
    新規プロジェクト作成時のデフォルトとなる emulator のオプション。
    既存プロジェクトには反映されないので、既存プロジェクトに対しては 2. か 3. の方法を実施。

  2. Additional Emulator Command Line Options を設定
    各プロジェクト毎の個別設定。
    1. を実施していれば、新規プロジェクトは自動設定されます。

  3. emulator コマンドの引数で指定。

詳細は以下の通りです。

1. Default emulator options を設定。

Eclipse にて "Window" --> "Preferences" で Preferences 画面を表示させます。
"Android" --> "Launch" を選択し、以下のように "Default emulator options" に値を設定します。
これで新規プロジェクトが作成されるたびに、この設定が反映されるようになります。



2. Additional Emulator Command Line Options を設定

Eclipse にて "Run" --> "Run Configurations..." 又は "Debug Configurations..." を選択。
左側の該当のプロジェクトを選択し、右側の "Target" タブを選択。
一番下にある "Additional Emulator Command Line Options" に値を設定します。

※ 自分の環境だけも知れませんが、画面を下に広げないと見えない状態でした。



3. emulator コマンドの引数で指定。

コマンド・プロンプトにて、emulator を起動するときに、
-http-proxy http://<proxyのFQDN又はIPアドレス>:<ポート番号> を指定します。

C:\> emulator -avd <AVD名> -http-proxy http://<proxyのFQDN又はIPアドレス>:<ポート番号>

2010年10月27日水曜日

subclipse でチェックアウトすると gen フォルダに関するエラーが出る

subclipse でチェックアウトを行うと、以下のようなエラーが出ます。
Project <プロジェクト名> is missing required source folder: 'gen'
The project cannot be built until build path errors are resolved

これは、Eclipse で以下のように操作すれば解消されます。
  1. Eclipse のメニューより、"Project" --> "Clean..." を選択。
  2. "Clean projects selected below" を選択し、該当のプロジェクトにチェックを入れ、
    "OK" を押します。


Eclipse でヘンテコなエラーが出たら、
Clean をやれば、大抵復旧される気がします ( ̄△ ̄)イイスギ?

2010年10月26日火曜日

subversion への接続を https 化することで proxy 経由でも接続可能になった

前回、外部から proxy 経由で自宅 subversion に接続しようとすると、
うまくできませんでした。

Proxyサーバに阻まれているんだけどどうしよう? を見ると、以下のような記述があります。
"別の戦略としては、SSL上でチェックアウトする、というのもあって、多くのプロキシがこれを許可している。"
ということで、 、Apache を https 化することにしました。

結論としては、SSL にすれば proxy 経由でも正常に subversion に接続することができました!

接続イメージ以下のような感じです。
外部PC --> proxy --> インターネット --> 自宅ルータ --> 自宅PC(Apache+SSL+subversion)

※ 自宅PCの環境は Windows Vista Ultimate SP2 32bit です。

以下に、OpenSSL 付の Apache の導入手順をメモしておきます。

  1. Apache 2.2.17 (OpenSSL付) ダウンロード
    Apache は以下のサイトより、
    httpd-2.2.17-win32-x86-openssl-0.9.8o.msi をダウンロードしました。

    http://httpd.apache.org/download.cgi


  2. 既存の Apache 2.2.16 (OpenSSL無) をアンインストール


  3. Apache 2.2.17 (OpenSSL付) インストール
    httpd-2.2.17-win32-x86-openssl-0.9.8o.msi を実行。

    手順は Apache 2.2.16 (OpenSSL無) と変わりません。
    詳細は Subversion & Apache & subclipse の導入 の "4. Apache のインストール" を参照。

    ※ httpd.conf は 2.2.16 アンインストール後も残っています。
        さらに、2.2.17 インストール後も残っているので、2.2.16 時点のものが、
        そのまま利用できました。


  4. 秘密鍵の作成
    コマンド・プロンプトを管理者として実行し、以下を実行。
    <Apache導入先>\conf\server.key を作成します。

    C:\> cd C:\Program Files\Apache Software Foundation\Apache2.2\bin

    C:\Program Files\Apache Software Foundation\Apache2.2\bin> openssl.exe genrsa -des3 1024 > ..\conf\server.key
    Loading 'screen' into random state - done
    Generating RSA private key, 1024 bit long modulus
    ............++++++
    ..................++++++
    e is 65537 (0x10001)
    Enter pass phrase:
    Verifying - Enter pass phrase:

    C:\Program Files\Apache Software Foundation\Apache2.2\bin>


  5. CSRの作成
    <Apache導入先>\conf\server.csr を作成します。

    C:\Program Files\Apache Software Foundation\Apache2.2\bin> openssl.exe req -config ..\conf\openSSL.cnf -new -key ..\conf\server.key > ..\conf\server.csr
    Enter pass phrase for ..\conf\server.key:
    Loading 'screen' into random state - done
    You are about to be asked to enter information that will be incorporated
    into your certificate request.
    What you are about to enter is what is called a Distinguished Name or a DN.
    There are quite a few fields but you can leave some blank
    For some fields there will be a default value,
    If you enter '.', the field will be left blank.
    -----
    Country Name (2 letter code) [AU]:JP
    State or Province Name (full name) [Some-State]:Fukuoka
    Locality Name (eg, city) []:Fukuoka
    Organization Name (eg, company) [Internet Widgits Pty Ltd]:rtaki
    Organizational Unit Name (eg, section) []:rtaki
    Common Name (eg, YOUR name) []:rtaki
    Email Address []:hoge@123.abc.co.jp

    Please enter the following 'extra' attributes
    to be sent with your certificate request
    A challenge password []:riverfall
    An optional company name []:rtaki

    C:\Program Files\Apache Software Foundation\Apache2.2\bin>


  6. サーバー証明書の作成
    <Apache導入先>\conf\server.crt を作成します。

    C:\Program Files\Apache Software Foundation\Apache2.2\bin> openssl.exe x509 -in ..\conf\server.csr -days 1000 -req -signkey ..\conf\server.key > ..\conf\server.crt
    Loading 'screen' into random state - done
    Signature ok
    subject=/C=JP/ST=Fukuoka/L=Fukuoka/O=rtaki/OU=rtaki/CN=rtaki/emailAddress=hoge@123.abc.co.jp
    Getting Private key
    Enter pass phrase for ..\conf\server.key:

    C:\Program Files\Apache Software Foundation\Apache2.2\bin>


  7. httpd.conf の編集
    SSLモジュールを有効化するために、httpd.conf にて、以下の行を有効化します。

      LoadModule ssl_module modules/mod_ssl.so


    さらに、SSL設定ファイル(以下の行)を有効化します。

      Include conf/extra/httpd-ssl.conf

    <Apache導入先>\conf\extra\httpd-ssl.conf は編集不要です。
    念のため、SSLCertificateFile と SSLCertificateKeyFile が、
    項番 4. と 6. で作成したファイルになっていることを確認しておきます。

    SSLCertificateFile "C:/Program Files/Apache Software Foundation/Apache2.2/conf/server.crt"
    SSLCertificateKeyFile "C:/Program Files/Apache Software Foundation/Apache2.2/conf/server.key"


  8. Apache 起動エラーの回避
    <Apache導入先>\conf\extra\server.key を server.key_bk にリネームし、以下を実行します。

    C:\Program Files\Apache Software Foundation\Apache2.2\bin> openssl.exe rsa -in ..\conf\server.key_bk -out ..\conf\server.key
    Enter pass phrase for ..\conf\server.key_bk:
    writing RSA key

    C:\Program Files\Apache Software Foundation\Apache2.2\bin>

    これを実行しないと Apache が起動に失敗し、<Apache導入先>\log\error.log に以下のエラーが記録されます。

    SSLPassPhraseDialog builtin is not supported on Win32 (key file C:/Program Files/Apache Software Foundation/Apache2.2/conf/server.key)


  9. 設定の反映
    httpd.conf の修正の修正を反映するために、Apache サービスを再起動します。
    「コントロールパネル」→「管理ツール」→「サービス」より、Apache 2.2 を再起動します。


  10. 接続確認
    ブラウザを起動し、https://localhost/ として、It works! と表示されること確認します。

    その際に、証明書に関する警告が表示されますが、無視して、
    「このサイトの閲覧を続行する (推奨されません)。」を選択します。(IE8の場合)

    IE8 以外でも同様の画面が表示されるので、無視しましょう。
    オレオレ証明書(自分で作った証明書)なので仕方がないです。



subclipse で最初に接続する際も、以下のように同じような警告が表示されます。
Error validating server certificate for https://xxx~
- Unkown certificate issuer
「永久に承諾」としておきましょう。



以上、https版Apacheの完成です。ヾ(〃^∇^)ノわぁい♪

2010年10月18日月曜日

proxy経由だと自宅subversionに接続できないことも

自宅に用意した subversion ですが、
とある外部から接続しようとした場合、proxy 経由だと接続できませんでした。
※ 2010.10.26 追記。https化することで proxy 経由でも接続できるようになりました。
    詳細は、こちら を参照下さい。

subclipse で接続しようとすると以下のようなエラーメッセージが出力されます。

RA layer request failed
svn: OPTIONS of 'http://www.xxx.yyy.zzz/svn/test_proj': 200 OK (http://www.xxx.yyy.zzz)

subversion の FAQ (http://subversion.apache.org/faq.html#proxy) を見ると、
以下の2つを確認せよとのことです。
  1. "%APPDATA%\Subversion\servers ファイルで Proxy 接続先を設定する。

  2. Proxy サーバーで、以下のHTTPメソッドが使用可であることを確認する。
    PROPFIND, REPORT, MERGE, MKACTIVITY, CHECKOUT。

1. を実施した上で、上記エラー・メッセージだったので、
おそらく、Proxy サーバーの設定の問題だと思っています。

それにしても、Eclipse 自体にも Proxy 設定があるので、
subclipse がそちらを使用せず、独自に設定を持っているのは、ちょっと不思議な感じです。。

Proxy サーバーの設定は、変更することも、確認することもできないので、
あきらめることにしました (ノ_・。)

proxy 経由だと、うまくいかないことがあるので要注意です。


2010/10/19追記。

ちなみに、コマンド・プロンプトから、
telnet コマンドで、Proxy が許可する HTTP メソッドを確認できます。

確かに、proxy サーバーから、405 で弾かれています。

青字が入力部分です。

> telnet [proxyサーバー] [proxyポート番号]

OPTIONS http://www.xxx.yyy.zzz/svn/test_proj/ HTTP/1.1
host:www.xxx.yyy.zzz
Authorization: Basic dXNlcjpwYXNzd29yZA==


HTTP/1.1 405 Method Not Allowed.
Allow: HEAD, GET, POST, CONNECT
Cache-Control: no-cache
Pragma: no-cache
Expires: Tue, 19 Oct 2010 09:34:33 GMT
Last-Modified: Tue, 19 Oct 2010 09:34:33 GMT
Content-Length: 1469
Content-Type: text/html
Accept-Ranges: bytes
Connection: close
Date: Tue, 19 Oct 2010 09:34:33 GMT
Server: proxy-server-xyz

~ボディ部省略~


Basic 認証時は、リクエスト・ヘッダーに Authorization を含めます。
値は ユーザー名:パスワード (間にコロン)を Base64 に変換します。

Base64の変換は以下のサイトを使用させて頂きました。

  Base64変換 - http://www.ahref.org/app/base64/base64.cgi

上記例では、user:password を Base64 に変換して、dXNlcjpwYXNzd29yZA== を指定しています。

2010年10月17日日曜日

グローバルIPアドレスを確認し、変更あればメールで通知

Subversion を外部から接続しようとした場合、
固定グローバルIPアドレスを使用しているわけではないので、
変更されてしまうと困ったことになります。

ちなみに、フレッツ光を自宅では使用しているのですが、
グローバルIPアドレスを固定にするには、タダではありませんし、
今のところ、お金を払うほど必要としているわけでもありません。

まあ、そうそう変わることはないと思うのですが、
変わったら、そのことをメールで通知させることはできないか?
ということを考えてみました。

メールで教えてくれれば、その都度、Subversion の接続を変えればいいと
思うわけです。


幸いなことに、内部(自宅内のPC)から接続すれば、グローバルIPアドレスが
何なのか教えてくれるWEBサイトがあります。

ということは、そのようなWEBサイトにアクセスして、
IPアドレスを抽出して、前回確認時と比較して、不一致だったらメール送信、
という処理を作成すればよいわけです。

んでもって、それをタスクで定期的に実行させれば、望み通りとなります。


以上の考えを元に、PowerShell スクリプトを作成してみました。

PowerShell は今回が初めてだったので、結構苦労しました・・・


グローバルIPアドレスを確認するWEBサイトは、以下を使用させて頂くことにしています。

自分のグローバルIPアドレス確認ツール、「IP君」


HTMLのBODY部に依存しているので、
確認に使用するWEBサイトが変われば、PowerShell スクリプト修正が必要です・・・

もし、ご利用される方がいらっしゃいましたら、
「# メール送信アドレス」の $smtp (メール・サーバー)、$to (メール送信先アドレス)、
$from (メール送信元アドレス) を修正してやってください。

get_GlobalIP.ps1
# 接続先URL
$url = "http://www.axisnetworks.biz/tools/gip/"
Write-Output "接続先: $url"

# メール送信先アドレス
$smtp = "xxxxx.ne.jp"
$to   = "xxxx@yyyyy.com"
$from = "xxxx@xxxxx.ne.jp"


# グローバルIPアドレス保存用ファイル
$path = Split-Path $MyInvocation.MyCommand.Path -Parent
$file = Join-Path $path get_GlobalIP.txt

# ファイル存在確認&前回確認時のIPアドレス読込み
If (Test-Path $file) {
  $old_address = Get-Content $file
} else {
  $old_address = "<記録なし>"
}
Write-Output "前回確認済みのグローバルIPアドレス: $old_address"


# GETリクエスト実行
$req = [System.Net.WebRequest]::Create($url)
$req.Method=”GET”
$resp=$req.GetResponse()

# レスポンスの読込み&確認
$encode = $resp.ContentEncoding
$sr = New-Object IO.StreamReader($resp.GetResponseStream(), $resp.ContentEncoding)

while ( ($line = $sr.ReadLine()) -ne $null ) {
  $line = $sr.ReadLine()
  if ($line -like '*<p class="style4" name="ip">*') {
    if ($line -match "(?<address>[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)") {
      $new_address = $matches.address
      if ($old_address -eq $new_address) {
        Write-Output "IPアドレス変更なし: $old_address == $new_address"
      } else {
        Write-Output "IPアドレスが変更: $old_address -> $new_address"
        # ファイル上書き
        Write-Output "ファイル更新: $file"
        $matches.address | Out-File -FilePath $file
        
        # メール送信
        Write-Output "メール送信: $to"
        $mail = New-Object Net.Mail.MailMessage($from, $to)
        $mail.Subject = "グローバルIPアドレス変更"
        $mail.Body = "IPアドレスが変更: $old_address -> $new_address"
        $mail.SubjectEncoding = [Text.Encoding]::GetEncoding("ISO-2022-JP")
        $mail.BodyEncoding = [Text.Encoding]::GetEncoding("ISO-2022-JP")
        $sc = New-Object Net.Mail.SmtpClient($smtp)
        $sc.Send($mail)
        $mail.Dispose()
      }
    }
  }
}

$sr.Close()
$resp.Close()


実行は、コマンド・プロンプトで以下のように実行します。

> powershell .\get_GlobalIP.ps1


最後に、powershell.exe、get_GlobalIP.ps1 ともにフルパスを指定して、
タスク登録すれば完成です。(= ̄▽ ̄=)V


ちなみに、これまでに(自分のように)PowerShell を実行したことがなければ、
以下のようなエラーが表示されるかもしれません。

スクリプトの実行がシステムで無効になっているため、ファイル C:\(配置場所)\get_GlobalIP.ps1 を読み込めません。詳細については、「get-help about_signing」と入力してヘルプを参照してください。


上記エラーが出た場合、PowerShell の実行ポリシーを変更する必要があります。
管理者で起動したコマンド・プロンプトにて、以下を実行します。

> powershell Set-ExecutionPolicy RemoteSigned

2010年10月16日土曜日

Subversion & Apache & subclipse の導入

複数の環境でソースの同期を行うことが面倒になってきたので、
Subversion を導入してみることにしました。

環境は Windows Vista Ultimate SP2 32bit と、
Eclipse Galileo 3.5.2 です。

導入設定は、以下の3つ行いました。

  • Subversion - バージョン 1.6.13
    バージョン管理を行うソフトウェア。
  • Apache - バージョン 2.2.16
    subversion を HTTP 経由でアクセスするために使用。
  • subclipse - バージョン 1.6.14
    Eclipse 用のプラグイン。
    Apache 経由で subversion にアクセスさせる。


以下が導入手順です。
  1. Subversion 及び Apache のダウンロード

    Subversion は以下のサイトより、Windows 版のバイナリ・パッケージをダウンロードします。

    http://subversion.apache.org/packages.html

    Windows 版のパッケージは、いくつかあるようですが、
    以下を導入することにしました。

     Win32Svn (32-bit client, server and bindings, MSI and ZIPs; maintained by David Darj)

    上記サイトより、Setup-Subversion-1.6.13.msi をダウンロードしました。


    Apache は以下のサイトより、
    httpd-2.2.16-win32-x86-no_ssl.msi をダウンロードしました。

     http://httpd.apache.org/download.cgi


  2. Subversion のインストール
    Setup-Subversion-1.6.13.msi を実行します。
  Next を押す。

  Next を押す。

  インストールするフォルダを指定。

  Install を押す。

  インストールが開始される。

  完了すると以下のような画面が表示されるので、Finish を押す。


  以上で、subversion のインストールは完了です。


  1. レポジトリ作成
    インストールが完了すると、自動的に環境変数 Path に、
    C:\Program Files\Subversion\bin が追記されます。

    コマンド・プロンプトを起動し、以下のコマンドを実行して、レポジトリを作成します。


    C:\>mkdir C:\svn

    C:\>svnadmin create C:\svn\test_project

    C:\>svn mkdir file://localhost/C:/svn/test_project/trunk -m "trunk作成"

    リビジョン 1 をコミットしました。

    C:\>svn mkdir file://localhost/C:/svn/test_project/tags -m "tags作成"

    リビジョン 2 をコミットしました。

    C:\>svn mkdir file://localhost/C:/svn/test_project/branchs -m "branchs作成"

    リビジョン 3 をコミットしました。

    C:\>

  2. Apache のインストール

    httpd-2.2.16-win32-x86-no_ssl.msi を実行します。
  Next を押す。

  accept を選択し、Next を押す。

  Next を押す。

  ドメイン名、サーバー名を localhost としておきます。
  メールアドレスも入力します。
  Apache をサービスとして稼動させるようにします。

  Next を押す。

  Next を押す。

  Install を押す。

  インストールが開始されます。

  インストールが正常に完了したら Finish を押す。


  以上で、Apache のインストールは完了です。


  1. Apache 接続確認
    ブラウザを起動し、http://localhost/ として、It works! と表示されること確認します。


  2. モジュールのコピー
    C:\Program Files\Subversion\bin\ にある mod_authz_svn.so と mod_dav_svn.soを、
    C:\Program Files\Apache Software Foundation\Apache2.2\modules\ にコピーします。


  3. httpd.conf の修正
    修正前の httpd.conf は念のためコピーして保存しておきます。
    メモ帳を管理者として実行します。

    まずは、以下の2行の先頭にある # を削除し、コメントアウトから有効にします。

      LoadModule dav_svn_module modules/mod_dav_svn.so
      LoadModule authz_svn_module modules/mod_authz_svn.so

    次に、以下の2行を新規に追加します。
    追加する場所は、LoadModule の記述のかたまりの下のあたりで構いません。

      LoadModule dav_svn_module modules/mod_dav_svn.so
      LoadModule authz_svn_module modules/mod_authz_svn.so

    最後に、httpd.conf ファイルの最後に、以下の行を追加します。

      <Location /svn>
        DAV svn
        SVNParentPath "C:\svn"

      </Location>


  4. Apache 再起動
    httpd.conf の修正の修正を反映するために、Apache サービスを再起動します。
    「コントロールパネル」→「管理ツール」→「サービス」より、Apache 2.2 を再起動します。


  5. ブラウザより表示されることを確認
    ブラウザを起動し、以下の URL を入力。

      http://localhost/svn/test_project/

    以下のように、test_project が表示されることを確認します。


  1. Eclipse に Subclipse ブラグインを導入
    Eclipse を起動し、「Help」→「Install New Software...」を選択。
    Add を押し、サイトを追加します。

    Name は subclipse とし、
    Location は http://subclipse.tigris.org/update_1.6.x とします。

  Core SVNKit Library、Optional JNA Library (recommended)、Subclipse の
  3 つともにチェックを入れ、Next を押します。

  Next を押します。

  I accept ~ にチェックを入れ、Finish を押します。

  インストールが開始されます。
  途中、認証に関する警告が表示されますが、OK と答えておきます。
  インストールが完了すると、Eclipse の再起動を促されるので、再起動します。

  1. Subclipse を使用してみる
    「Window」→「Open Perspective」→「Other...」より、
    「SVN リポジトリー・エクスプローラー」を開きます。

  SVN Repositories が表示されるので、右クリックから、
  「新規」→「リポジトリー・ロケーション」を選択します。

  項番 9. でブラウザにて接続確認した URL を入力します。
  http://localhost/svn/test_project/

  無事、レポジトリーに接続できました。


  1. 既存プロジェクトのチェックイン
    「Package Explorer」より既存プロジェクトを右クリックし、
    「Team」→「Share Project..,」より「SVN」を選択することで行えます。


以上、導入までの手順を整理しました。

う~ん、これでソース管理が楽になればいいんだけど。


2010年10月14日木曜日

SDカードの中身を ListView を使って表示&選択 (拡張子フィルター付)

こんな感じのサンプル・アプリを作ってみました。

起動直後
起動直後に、/sdcard 内のディレクトリ及びファイル(拡張子が.xmlのみ)を表示します。

上記画面で、dir1 を選択すると、以下のように、/sdcard/dir1 内が表示されます

1つ下に移動し、ファイル選択
test1_1.xml を選択すると、EditText にフルパス名が自動セットされ、
最下段にある OK ボタンが Enable になります。

/sdcard 直下では表示されませんが、
/sdcard/dir1 など、/sdcard のサブ・ディレクトリでは上位ディレクトリに戻るために、
"...(上へ)" を表示&選択できるようにしています。

ちなみに、SDカードへの書き込みを行う際は、AndroidManifest.xml に、
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
を記述する必要があります。


以下はソースです。

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"
    >

<TextView
    android:id="@+id/current_dir"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    />

<ListView
    android:id="@+id/select_file"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_weight="2"
/>

<TextView
    android:text="ファイル名:"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    />

<EditText
    android:id="@+id/file_text"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    />

<Button
    android:id="@+id/ok_button"
    android:text="OK"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    />

</LinearLayout>



SdCardDisplayActivity.java
package jp.kochi;

import java.io.File;
import java.io.FileFilter;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Environment;
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.TextView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;

public class SdCardDisplayActivity extends Activity implements OnItemClickListener {

    private ListView mListView;
    private EditText mEditText;
    private TextView mCurDirTextView;
    private File mFile;
    private String mWarning;
    private String mGoUp;
    private File mSDdir;
    private Button mButton;

      @Override
      public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

          setContentView(R.layout.main);

          mCurDirTextView = (TextView) findViewById(R.id.current_dir);
          mButton = (Button) findViewById(R.id.ok_button);
          mButton.setEnabled(false);

      mWarning = "警告";
      mGoUp    = ".. (上へ)";

          //final File SDdir
          mSDdir = Environment.getExternalStorageDirectory();
      Log.v("DEBUG", "getExternalStorageDirectory() = " + mSDdir.getPath());

          String SDstatus = Environment.getExternalStorageState();
      Log.v("DEBUG", "getExternalStorageState() = " + SDstatus);

      // SDカードがセットされているか確認
      if (SDstatus.equals(Environment.MEDIA_MOUNTED) == false &&
          SDstatus.equals(Environment.MEDIA_MOUNTED_READ_ONLY) == false) {

        AlertDialog.Builder builder = new AlertDialog.Builder(SdCardDisplayActivity.this);

        builder.setTitle(mWarning);
        builder.setMessage("SDカードがセットされていません。");
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener () {

          @Override
          public void onClick(DialogInterface dialog, int which) {
            Log.v("DEBUG", "alerDialog - OnClickListener()");
            SdCardDisplayActivity.this.finish();
          }
        });

        builder.show();

        Log.v("DEBUG", "finish: " + this.getClass().getName());

      }

          mListView = (ListView) findViewById(R.id.select_file);
          mListView.setOnItemClickListener(this);
          //list.setAdapter(new ListAdapter)
          updateList(mSDdir);

          Button select = (Button) findViewById(R.id.ok_button);
          select.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {
          Toast.makeText(SdCardDisplayActivity.this, mEditText.getText(), Toast.LENGTH_LONG).show();
          // --------------------------
          // ファイル読込み等の処理を行う
          // --------------------------
        }
      });


          mEditText = (EditText) findViewById(R.id.file_text);
      }

      final private FileFilter filter = new FileFilter() {
      @Override
      public boolean accept(File pathname) {
        if (pathname.isDirectory() == true) {
          return true;
        } else if(pathname.getName().endsWith(".xml") == true){
          return true;
        }
        return false;
      }
    };

      private void updateList(File dir){

        // EditText に選択されたファイル名/フォルダ名をセット
        mCurDirTextView.setText(dir.getAbsolutePath());

        SimpleAdapter adapter = new SimpleAdapter(this, getData(dir.listFiles(filter), dir.getParentFile()),
                  android.R.layout.simple_list_item_1, new String[] { "name" },
                  new int[] { android.R.id.text1 });

        mListView.setAdapter(adapter);
      }

    private List getData(File[] files, File parent) {

      Map<String, Object> temp;
      List<Map> myData = new ArrayList<Map>();

      if (parent != null && mSDdir.getParent().equals(parent.getAbsolutePath()) == false) {
        temp = new HashMap<String, Object>();
        temp.put("name", mGoUp);
        temp.put("file", parent);
        myData.add(temp);
      }

      if (files != null) {
        for(File file : files){
          temp = new HashMap<String, Object>();
          if (file.isDirectory()) {
            temp.put("name", file.getName() + "/");
          } else {
            temp.put("name", file.getName());
          }
          temp.put("file", file);
          myData.add(temp);
        }
      }

      Collections.sort(myData, sDisplayNameComparator);

      return myData;
    }

    private final static Comparator<Map> sDisplayNameComparator = new Comparator<Map>() {
      private final Collator collator = Collator.getInstance();

      public int compare(Map map1, Map map2) {
        return collator.compare(map1.get("name"), map2.get("name"));
      }
    };

    @Override
    public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
      Map map = (Map) arg0.getItemAtPosition(arg2);
      mFile = (File) map.get("file");

      mEditText.setText(mFile.getAbsolutePath());
      if (mFile.isDirectory()) {
        mButton.setEnabled(false);
        updateList(mFile);
      } else {
          // ファイルだったら、ボタンを押せるようにする
          mButton.setEnabled(true);
      }


    }
  }