ラベル Coding Tips の投稿を表示しています。 すべての投稿を表示
ラベル Coding Tips の投稿を表示しています。 すべての投稿を表示

2014年12月19日金曜日

Google APIs Client Library for JavaScriptを使いながらfieldsを指定するには

Google APIs Client Library for JavaScriptを使っていて、必要ないレスポンスデータを削除する為にfieldsを指定するときの忘備録。

検索やデータ取得などのGET系では、パラメータの一部として指定すればいい。

gapi.client.drive.files.list({
  'fields': 'items(id,title),selfLink',
  'q': "title='" + folderTitle + "' and trashed=false"
}).execute(function(folder) {
  console.debug(folder.selfLink);
});


作成、更新などのPUT系では、Request bodyをresponseフィールドに定義するが、fieldsは、responseの中ではなく、外に指定する。この点が注意ポイント。

gapi.client.drive.files.insert({
  'fields': 'id',
  'resource': {
    mimeType: 'application/vnd.google-apps.folder',
    title: folderTitle
  }
}).execute(function(folder) {
  console.debug(folder.id);
});

2013年3月14日木曜日

MediaProviderのgetType()でIllegalStateExceptionが発生して困ったらmimeTypeの指定を確認すべし

画像をダウンロードした際に、そのメタ情報をMediaProviderへ登録するために、MediaScannerConnectionを使っている。
MediaScannerConnection.scanFile(context,
          new String[] { uri.getPath() },
          new String[] { "image/*" },
          new MediaScannerConnection.OnScanCompletedListener() {
            public void onScanCompleted(String path, Uri uri) {
              // Notificationを表示する
              notifyDownloadComplete(context, path, uri, sms);
            }
          });

ところが、MediaProviderのgetType()でIllegalStateExceptionが発生して、Activity Managerがクラッシュしてしまうことが多くて困った。




原因は、MediaScannerConnectionのscanFile()へmimeTypeを指定しているところ。
new String[] { "image/*" },

mimeType判断として、"image/*"と指定して、「何らかの画像」と加えたのが、逆にmimeType判断に時間をかけさせてしまい、IllegalStateException発生の温床にしてしまっていたらしい。

mimeTypeが特定できない場合にスキャンさせる時は、mimeTypeを指定せず、nullを与えて、拡張子から自動的に判断させるようにする方が好ましいようだ。


よって、mimeTypeの指定をnullへ変更。
MediaScannerConnection.scanFile(context,
          new String[] { uri.getPath() },
          null,
          new MediaScannerConnection.OnScanCompletedListener() {
            public void onScanCompleted(String path, Uri uri) {
              // Notificationを表示する
              notifyDownloadComplete(context, path, uri, sms);
            }
          });


面積や、JPEG、PNGなど種類を変えて、様々な画像のダウンロードを試してみたが、変更後は、IllegalStateExceptionは全く発生しなくなった(^。^)

2013年2月4日月曜日

DownloadManagerのDOWNLOAD_COMPLETEアクションに対応するBroadcastReceiverを書いていてふと疑問に思ったこと

DownloadManagerのDOWNLOAD_COMPLETEアクションに対応するBroadcastReceiverを書いていて、ふと疑問。

DownloadManagerはシステムレベルで提供されているので、DOWNLOAD_COMPLETEアクションが発生した場合、自分のアプリだけにかかわらず、対応するアプリ全てにアクションがブロードキャストされるのではないかと。

そうだとしたら、フィルタリングする何らかの処理を書いておかないと、他のアプリが叩いたDownloadManagerのDOWNLOAD_COMPLETEアクションに対しても、自分のアプリのBroadcastReceiverが起動してしまうのではないかと。


試しに、DOWNLOAD_COMPLETEアクションに対応するBroadcastReceiverの記述を持つアプリをコピーして、それぞれのアプリをインストール。DOWNLOAD_COMPLETEアクションに対応して両方のBroadcastReceiverが起動するのか確認してみた。

DownlaodManagerにキュー入れする部分のコード。
Request req = new Request(uri);
req.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
req.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, subPath);
req.setMimeType(contentType);
req.setTitle(getResources().getString(R.string.app_name));
req.setDescription(getResources().getString(R.string.dl_start_str));
Log.v(getTag(), "ダウンロード開始:" + uri);

//ダウンロードマネージャにダウンロードリクエストをキュー
DownloadManager dlman = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
dlman.enqueue(req);

DOWNLOAD_COMPLETEアクションをレシーブするDownloadManagerBroadcastReceiverクラス。
public class DownloadManagerBroadcastReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {

  // 関係のあるメッセージがどうか調べる
  String pkg = intent.getPackage();
  if (pkg.equals("my.app.package")) {
       Log.v(getTag(), "関係あるブロードキャスト受信:" + intent);
  } else {
       Log.v(getTag(), "無関係なブロードキャスト受信:" + intent);
       return;
  }

} // onReceive

} // class

my.app.packageのAndroidManifest.xml抜粋




 


my.app.package_dummyのAndroidManifest.xml抜粋






my.app.package_dummyのDownloadManagerBroadcastReceiverクラス。
public class DownloadManagerBroadcastReceiver extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {

    // 関係のあるメッセージがどうか調べる
    String pkg = intent.getPackage();
    if (pkg.equals("my.app.package_dummy")) {
       Log.v(getTag(), "関係あるブロードキャスト受信:" + intent);
    } else {
       Log.v(getTag(), "無関係なブロードキャスト受信:" + intent);
       return;
    }

  } // onReceive

} // class


結果、my.app.packageでDOWNLOAD_COMPLETEアクションが発生した場合は、my.app.package.DownloadManagerBroadcastReceiverが起動、

関係あるブロードキャスト受信:Intent { act=android.intent.action.DOWNLOAD_COMPLETE flg=0x10 pkg=my.app.package cmp=my.app.package/.DownloadManagerBroadcastReceiver (has extras) }


my.app.package_dummyでDOWNLOAD_COMPLETEアクションが発生した場合は、my.app.package_dummy.DownloadManagerBroadcastReceiverが起動した。

関係あるブロードキャスト受信:Intent { act=android.intent.action.DOWNLOAD_COMPLETE flg=0x10 pkg=my.app.package_dummy cmp=my.app.package_dummy/.DownloadManagerBroadcastReceiver (has extras) }


両方のDownloadManagerBroadcastReceiverが同時に起動することはなかった。

つまり、それぞれのアプリ内でDownloadManagerを使ってダウンロードした時に発生するDOWNLOAD_COMPLETEアクションには、それぞれのマニフェストに明記しているBroadcastReceiverクラスが対応することになる。


onReceive()内でパッケージ名をチェックする必要などはない。

ということがわかった。


では、さらに、同一パッケージ内で、DOWNLOAD_COMPLETEアクションに対応するBroadcastReceiverが複数登録されていたらどうなる?というテストをしてみた。

AndroidManifest.xml抜粋





        






02-04 16:23:35.943: V/DownloadManagerBroadcastReceiver.onReceive:42(1695): 関係あるブロードキャスト受信:Intent { act=android.intent.action.DOWNLOAD_COMPLETE flg=0x10 pkg=my.app.package cmp=my.app.package/.DownloadManagerBroadcastReceiver (has extras) }
02-04 16:23:36.033: V/DownloadManagerBroadcastReceiver2.onReceive:42(1695): 関係あるブロードキャスト受信:Intent { act=android.intent.action.DOWNLOAD_COMPLETE flg=0x10 pkg=my.app.package cmp=my.app.package/.DownloadManagerBroadcastReceiver2 (has extras) }

両方のBroadcastReceiverが呼ばれた。起動順序はマニフェストに記述した順序かな?

2012年3月13日火曜日

Notificationをタップしてギャラリーで画像を開く

画像のダウンロードが完了した際にNotificationを表示している。

Notificationをタップして確認のため画像を開くときに、標準のギャラリーで開きたいなと思って書いてみたコード。

protected void onPostExecute(String description) {
 if (error) {

  showNotification(DOWNLOAD_CANCELED,"Data download ended abnormally!", "", null);

 } else {

  //開いてみるだけなのでACTION_VIEWをセット
  Intent intent = new Intent(Intent.ACTION_VIEW);

  intent.setType("image/*");

  //ターゲットの画像URIを設定
  intent.setData(download_uri);

  //PendingIntentを作成
  PendingIntent contentIntent = PendingIntent.getActivity(myAppContext, 0, intent, 0);

  //Notificationを表示
  showNotification(DOWNLOAD_DONE, "Data download is complete!",description, contentIntent);

 }
}

private void showNotification(int mode, String contentTitle,String contentText, PendingIntent contentIntent) {

 Notification notification = null;
 long now = System.currentTimeMillis();
 switch (mode) {
 case DOWNLOADING:
  notification = new Notification(android.R.drawable.stat_sys_download, contentTitle, now);
  notification.defaults |= Notification.DEFAULT_LIGHTS;
  notification.defaults |= Notification.DEFAULT_SOUND;
  notification.flags = Notification.FLAG_ONGOING_EVENT;
  break;
 case DOWNLOAD_DONE:
  notification = new Notification(android.R.drawable.stat_sys_download_done, contentTitle,now);
  notification.flags = Notification.FLAG_AUTO_CANCEL;
  break;
 case DOWNLOAD_CANCELED:
  notification = new Notification(android.R.drawable.ic_menu_close_clear_cancel,contentTitle, now);
  notification.flags = Notification.FLAG_AUTO_CANCEL;
  break;
 }

 notification.setLatestEventInfo(myAppContext, contentTitle,contentText, contentIntent);
 notificationManager.notify(notification_id, notification);
}

Notificationをタップすると・・・

ダウンロード後の画像がギャラリーで開きます。


Notificationとギャラリーを絡めたサンプルがあまり見当たらなかったので忘備録としてメモメモ。

2012年3月9日金曜日

Cursorのclose()が原因で Attempted to access a cursor after it has been closed

Activity.managedQuery()でCursorを取得し、データ取得後にCursorをclose()していたら、close()のタイミングが悪くてRuntimeExceptionが発生。

java.lang.RuntimeException: Unable to resume activity {jp.hogehoge.test/jp.hogehoge.test.DownloadActivity}: android.database.StaleDataException: Attempted to access a cursor after it has been closed.

ソースの抜粋
@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.download);

 // MediaStoreのDBを保存日の降順で検索
 Cursor cursor = managedQuery(URI, PROJECTIONS, WHERE,WHERE_PARAM, MediaStore.Images.Media.DATE_ADDED+ " desc");

 if (cursor.moveToFirst()) {
  do {
  } while (cursor.moveToNext());
 }
 cursor.close();

}// onCreate()

他のActivityを前面に出し、再度、DBからの一覧を表示した元の画面へ戻ってきたタイミングで発生する。

原因は、onCreate()の中でcursor.close()していたこと。

Activityのライフサイクルからいうと、再度前面へ戻ってきた時にonCreate()はコールされないので、DBからの一覧を取得できず「a cursor after it has been closed」となるわけだ。ごもっとも。

close()を明示的にコールしなければExceptionは発生しなくなった。


だがしかし、close()しないのはお作法として良いのか?と思い検索してみたら、Stack Overflowに参考になるポストがあった。
参考:managedQuery() vs context.getContentResolver.query() vs android.provider.something.query() - Stack Overflow

managedQuery() will use ContentResolver's query(). The difference is that with managedQuery() the activity will keep a reference to your Cursor and close it whenever needed (in onDestroy() for instance.) If you do query() yourself, you will have to manage the Cursor as a sensitive resource. If you forget, for instance, to close() it in onDestroy(), you will leak underlying resources (logcat will warn you about it.)

managedQuery() では、Activityがカーソルへの参照を保持していて必要なくなった時(onDestroy()が呼ばれた時)にクローズされる、とある。よって、明示的にclose()しなくてもよさそうだ。

ちなみに、ContentResolver.query()を使った場合は、自分で明示的にクローズしないとリークが起こるよとある。メモメモ。


managedQuery() はdeprecatedなので、本来なら使いたくないのだが、代替となるCursorLoaderはHoneycomb以上でしか使えない。

まだまだGingerbread搭載機種が発売されている現状では自分でコンパチビリティを書かないといけない。面倒なので使っていない\(^o^)/

Activity.managedQueryの絞り込み条件はどう指定するの?

ActivityのmanagedQueryで絞り込み条件はどう指定するの?

と思った時に、あまり絞り込み条件を含んだサンプルが無かったので忘備録としてメモ。

private static final Uri URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
// 検索する列を指定
private static final String[] PROJECTIONS = { MediaStore.Images.Media._ID,MediaStore.Images.Media.DATE_ADDED, MediaStore.Images.Media.SIZE, "width", "height" };

// where句(絞り込み条件)を指定
private static final String WHERE = MediaStore.Images.Media.BUCKET_DISPLAY_NAME + "=?";

// where句の値を設定
private static final String[] WHERE_PARAM = { "hogehoge" };

@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.download);

 // MediaStoreのDBを保存日の降順で検索
 Cursor cursor = managedQuery(URI, PROJECTIONS, WHERE,WHERE_PARAM, MediaStore.Images.Media.DATE_ADDED + " desc");

要は、SQLiteの文法に則って記述すれば良かったのね。
参考:SQLiteでデータベース - 愚鈍人

絞り込み条件が複数ある場合は、

xxx = ? and yyy =?

と条件を指定し、記述した順番にString[]へパラメータを列挙する。

String[] WHERE_PARAM = { "xxxのパラメータ", "yyyのパラメータ" };

2012年3月8日木曜日

SQLiteの時刻は秒が基本のUnix Timeで管理されている。1000をかけてミリセカンドへ変換するのがミソ。

AndroidのSQLiteに保存した日時を取得する際、1970年1月X日という表示になってちょっとハマった。

ポイントは、SQLiteの時刻はエポックタイム(1970年1月1日)から「何秒」経過しているかというUnix Timeで管理されているが、java.util.Date.getTime()が返すlong値はエポックタイムから「何ミリ秒」経過しているかを計算しているということだった。

単純に1000をかけることで解決。
こんな感じ。

if (cursor.moveToFirst()) {
  do {
    // 保存日を取得
    long date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED));

    // SQLiteは秒ベースのUNIX時間で管理されているので1000を掛けてミリセカンドへ単位変更
    date *= 1000;

    // 年月日時分で保存日をフォーマット
    CharSequence dateClause = DateUtils.formatDateTime(getApplicationContext(), date,DateUtils.FORMAT_SHOW_TIME |  DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR);

    // 保存日をTextViewに設定
    TextView download_tv_date_added = (TextView) row.findViewById(R.id.download_tv_date_added);
    download_tv_date_added.setText(dateClause);
  } while (cursor.moveToNext());
}
cursor.close();

2012年2月16日木曜日

GoogleのC2DMサーバーのセキュリティ例外を無視させる

GoogleのC2DMサーバーはオレオレ証明書を使っているので、Javaのプログラムでメッセージをを送るとき、https経由だとSSLHandshakeExceptionが発生してしまう。

それを回避するプログラム。

private static final String SERVER_URL = "https://android.apis.google.com/c2dm/send";

java.net.URL c2dmUrl = new java.net.URL(SERVER_URL);

javax.net.ssl.HttpsURLConnection conn = (javax.net.ssl.HttpsURLConnection) c2dmUrl.openConnection();
conn.setDoOutput(true);

// C2DMサーバーの自己証明書の例外を無視する
javax.net.ssl.KeyManager[] km = null;
javax.net.ssl.TrustManager[] tm = {
 new javax.net.ssl.X509TrustManager() {
  public void checkClientTrusted(java.security.cert.X509Certificate[] arg0, String arg1) throws java.security.cert.CertificateException {}
  public void checkServerTrusted(java.security.cert.X509Certificate[] arg0, String arg1) throws java.security.cert.CertificateException {}
  public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; }
 }
};
javax.net.ssl.SSLContext sslcontext= javax.net.ssl.SSLContext.getInstance("SSL");
sslcontext.init(km, tm, new java.security.SecureRandom());
conn.setSSLSocketFactory(sslcontext.getSocketFactory());

この処置でもまだ'HTTPS hostname wrong:'エラーが発生する。証明書のホスト名とアクセスしているホスト名も違うようだ。

よって更にホスト名違いも無視するように設定を追加。

conn.setSSLSocketFactory(sslcontext.getSocketFactory());

// 証明書にあるホスト名とアクセスしているホスト名の違いを無視する
conn.setHostnameVerifier(
 new javax.net.ssl.HostnameVerifier() {
  public boolean verify(String host, javax.net.ssl.SSLSession ses) { return true; }
 }
);


もう、どれだけずさんな証明書なんだか ┐('~`;)┌

2011年7月9日土曜日

画像読み込み完了時に画面幅によって画像をリサイズする

作成した画像を、スマートフォンのブラウザで見るときに、画面幅にあわせてリサイズさせてみた。

ブラウザの画面幅を取得するコードは以下のページを参考にした。
参考:ブラウザの画面サイズの取得(javascript) - Object Design Blog

画像の読み込み完了前に拡大縮小率を計算しないように、img の load 完了イベント時に、画像の幅を取得するのがポイント。以下、コードの一部抜粋。

<head>

<script type="text/javascript">
var imgW = 0;
var imgH = 0;
var paneW = 0;
var scale = 100;

//ブラウザ幅に合わせて拡大縮小率を算出する
//マージンは20px
function calcScale(){
  var paneW_alt = paneW - 20;
  if(imgW > paneW_alt){
    scale= ((paneW_alt/imgW)*100).toFixed(0);
  }else{
    scale = 100;
  }
}

function getBrowserWidth() {  
    if(window.innerWidth) {
        return window.innerWidth; 
    } 
    else if(document.documentElement && document.documentElement.clientWidth != 0){
        return document.documentElement.clientWidth;
    }
    else if(document.body){
        return document.body.clientWidth;
    }
    return 0;
}

</script>

</head>

<body marginheight="0" marginwidth="0">


(中略)


<script type="text/javascript"><!--
var img = document.getElementById("dlImg");

//画像のload完了時に画像の幅と高さを取得する
img.addEventListener('load', getImageSize, false);
function getImageSize(){
    imgW = img.width;
    imgH = img.height;
}

//画像の幅と高さを取得した上で画像をリサイズする
img.addEventListener('load', resize, false);

//端末が回転された際もブラウザ幅に合わせて再度リサイズする
window.addEventListener('orientationchange', resize, false);

function resize(){
    paneW = getBrowserWidth();
    calcScale();
    if(imgW > 0 && imgH > 0){
      img.width=(imgW * (scale/100)).toFixed(0);
      img.height=(imgH * (scale/100)).toFixed(0);
    }
}
//--></script>

</body>

2010年12月21日火曜日

Android アプリでGoogle Maps のズームイン/アウト時に何らかのイベントを処理したい場合はOnZoomListener を実装する

Androidアプリで、Google Mapsのズームイン/ズームアウト時に何らかのイベントを処理したい場合は、OnZoomListener を実装する。

import android.os.Bundle;
import android.util.Log;
import android.widget.ZoomButtonsController.OnZoomListener;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;

public class MapDemoActivity extends MapActivity implements OnZoomListener {

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

 private void createMapView() {
  mapView = (MapView) findViewById(R.id.mapview);
  // デフォルトのズームコントローラーを使用する
  mapView.setBuiltInZoomControls(true);
  // ズームコントローラーのイベントを処理するリスナーを設定する
  mapView.getZoomButtonsController().setOnZoomListener(this);
 }

 @Override
 public void onZoom(boolean zoomIn) {
  if (zoomIn ? mapView.getController().zoomIn() : mapView.getController()
    .zoomOut());
  Log.i(LOGTAG, "onZoom():zoomIn=" + zoomIn);

  // タスクを実行する
  executeTask();
 }

 @Override
 public void onVisibilityChanged(boolean visible) {
 }

onZoomListener を実装すると、必ず、onZoom() とonVisibilityChanged() をオーバーライドしなければならない。

onZoom() は、正に、ズームイン/ズームアウト時に呼ばれるメソッドであり、onVisibilityChanged() は、デフォルトのズームコントローラーの表示/非表示が切り替わったときに呼ばれるメソッド。特に行いたい処理が無くても、これら2つのメソッドは記述する必要がある。


注意点としては、onZoom() の処理をオーバーライドするので、自分でzoomIn() やzoomOut() を呼ばなくてはいけなくなること。zoomIn()/zoomOut() をコールしないと、マップがズームイン/ズームアウトしなくなる。

2010年11月9日火曜日

AsyncTaskを使った場合ItemizedOverlayのリストを更新するときはMapViewのinvalidate()を忘れずに

AsyncTaskクラスのサブクラスを作って、バックグラウンドでリクエストデータを処理し、MapView上のItemizedOverlayを更新する処理を行っていた。

ところが、タップしないとアイテムがマップ上に描画されないという症状にはまり、試行錯誤。

結局、MapViewにアタッチされたItemizedOverlayを更新した際にはMapViewのinvalidate()をコールして強制的にビューを再描画させる、ということが肝だった。

参考にしたのは以下のページ。
Maps/NooYawkAsync/src/com/commonsware/android/maps/NooYawk.java at master from commonsguy's cw-advandroid - GitHub
public void onPreExecute() {
   if (sites!=null) {
    map.getOverlays().remove(sites);
    map.invalidate(); 
    sites=null;
   }
  }

  @Override
  public Void doInBackground(Void... unused) {
   SystemClock.sleep(5000);      // simulated work

   sites=new SitesOverlay();

   return(null);
  }

  @Override
  public void onPostExecute(Void unused) {
   map.getOverlays().add(sites);
   map.invalidate();   
  }

上記のソースでは、onPreExecute()でItemizedOverlayを削除し、onPostExecute()で追加するという処理が行れている。

しかし、試してみたところ、その処理は必要ない。
ItemizedOverlayはアプリケーション起動時からMapViewに追加しっぱなしでアイテムリストの更新ができた。

ただ肝は、onPostExecute()のinvalidate()だ。これは忘れちゃいけない。


以下はMyソース。
@Override
 protected void onPostExecute(ArrayList<OverlayItem> result) {

  GeoItemizedOverlay geoOverlay = (GeoItemizedOverlay) mapView
    .getOverlays().get(0);

  // 現在オーバーレイに追加されているアイテムを削除する
  geoOverlay.clear();
  geoOverlay.populateOverlay();

  // オーバーレイにアイテムを追加してpopulate()を呼ぶ
  geoOverlay.addAll(result);
  geoOverlay.populateOverlay();
  mapView.invalidate();
 }

ドキュメントを見ると、ちゃんとinvalidate()がビューを強制的に再描画させるメソッドであることが書いてある。
How Android Draws Views | Android Developers

You can force a View to draw, by calling invalidate().

ドキュメントはよーくチェックしないといけませんな。

2010年11月1日月曜日

Buzzの日付書式のフォーマットパターンをTimeクラスのparse3339()を使って簡単にパースする

Google Buzz APIで結果を取得したときの日付書式「2010-10-15T06:51:04.000Z」を、SimpleDateFormatクラスを使って解析する方法について以前書いたが、android.text.format.Timeクラスのparse3339()を使うと、もっと簡単に解析できることが分かった。

public class Message {

 private static java.text.DateFormat FORMAT_DATE;
 private static java.text.DateFormat FORMAT_TIME;
 private Time published;

 public Message(java.text.DateFormat df, java.text.DateFormat tf) {
  FORMAT_DATE = df;
  FORMAT_TIME = tf;
  published = new Time();
 }

 public void setPublished(String published) {
  try {
   this.published.parse3339(published.trim());
  } catch (TimeFormatException e) {
   throw new RuntimeException(e);
  }
 }

 public String getPublished() {
  Date date = new Date(published.normalize(true));
  return FORMAT_DATE.format(date) + " " + FORMAT_TIME.format(date);
 }

RFC3339については、以下にドキュメントがある。
RFC3339 - Date and Time on the Internet: Timestamps


コンストラクタの引数のjava.text.DateFormatで、端末側で選択されているロケール・書式に則った、日付と時間の書式を渡している。これによって、ユーザが設定している書式に則って、日付と時間をフォーマットしている。

Message message = new Message(android.text.format.DateFormat.getDateFormat(context), android.text.format.DateFormat.getTimeFormat(context));

ItemizedOverlay 使用時にたまにArrayIndexOutOfBoundsException が発生するバグへの対応

com.google.android.maps.ItemizedOverlayを使って地図上にアイテムを配置すると、アイテムをタップした際に、たまにArrayIndexOutOfBoundsExceptionが発生して困った。

この問題を解決するには、setLastFocusedIndex(-1)をpopulate()の前にコールする。

APIドキュメントによると、フォーカスが変わったときに自動的にアップデートされるので、ItemizedOverlayを継承したサブクラスで、フォーカスのインデックスを変更する必要はない、と書かれているが・・・。

実際は、リスト更新時に、populate()の前に自分でsetLastFocusedIndex(-1)を呼び、どのアイテムにもフォーカスが設定されていない状態を作らないといけない。

public class GeoItemizedOverlay extends ItemizedOverlay<OverlayItem> {

 private ArrayList<OverlayItem> overlayArrayList = new ArrayList<OverlayItem>();

 public void add(OverlayItem overlay) {
  overlayArrayList.add(overlay);
 }

 public void clear() {
  overlayArrayList.clear();
 }

 public void populateOverlay() {

  // 必ずpopulate()の前にsetLastFocusedIndex(-1)を実行する
  // そうしないと、リストが変わった際にアイテムのインデックスがずれる場合がありArrayIndexOutOfBoundsExceptionが発生することがある
  // http://groups.google.com/group/android-developers/browse_thread/thread/38b11314e34714c3
  setLastFocusedIndex(-1);
  populate();
 }
}

オーバレイアイテム更新時には、clear()やadd()し終わった際には必ずpopulateOverlay()を呼ぶようにする。

// 現在オーバーレイに追加されているアイテムを削除する
geoOverlay.clear();
// 必ずsetLastFocusedIndex(-1)とpoopulate()を実行
geoOverlay.populateOverlay();

for (int i = entries.getLength() - 1; i >= 0; i--) {

 OverlayItem overlayItem = new OverlayItem(・・・);
 // アイテムを追加
 geoOverlay.add(overlayItem);

}

// 必ずsetLastFocusedIndex(-1)とpoopulate()を実行
geoOverlay.populateOverlay();

解決には以下の記事を参考にさせていただいた。
ItemizedOverlay & ArrayIndexOutOfBoundsException > explanation AND solution !! - Android Developers | Google グループ

Android – ItemizedOverlay + ArrayIndexOutOfBoundsException / NullPointerException workarounds « Developmentality


先日のNullPointerExceptionが発生する件といい、ItemizedOverlayを使う際には注意が必要です。

2010年10月26日火曜日

タッチイベントはMapViewとItemizedOverlayのどちらにまず伝わるのか

Android+Google MAP APIでタッチイベントを処理する時のイベントの流れを確認する。
使ったのは以下のクラス。

・MapDemoActivityクラス
MapActivityを継承したクラス。実行クラス。

・CusomizedMapViewクラス
MapViewを継承したクラス。MapDemoActivityのContentViewとして設定。

・GeoItemizedOverlayクラス
ItemizedOverlayを継承したクラス。


これらのクラスは、下から積み上げると以下のような階層関係にある。

GeoItemizedOverlay
|
CusomizedMapView
|
MapDemoActivity


・検証

ディバイスの画面にタッチしたときには、onTouchEvent(MotionEvent event)が呼び出される。

MapViewを使ったときは、Activityに記述したonTouchEventではなくMapView側のonTouchEventが呼び出されることは、前の記事に書いた。

では、MapViewにItemizedOverlayが追加されているときは、タッチイベントはMapViewとItemizedOverlayのどちらにまず伝わるのか。

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

  /** マップビュー作成 */
  CustomizedMapView mapView = (CustomizedMapView) findViewById(R.id.mapview);

  Drawable drawable = this.getResources().getDrawable(R.drawable.star);
  GeoItemizedOverlay geoOverlay = new GeoItemizedOverlay(drawable, this);

  /** オーバーレイをマップに追加 */
  List<Overlay> mapOverlays = mapView.getOverlays();
  mapOverlays.add(geoOverlay);
 }
}
public class CustomizedMapView extends MapView {
 @Override
 public boolean onTouchEvent(MotionEvent event) {
  Log.i("onTouchEvent", "CustomizedMapView.onTouchEvent");
  return super.onTouchEvent(event);
 }
}
public class GeoItemizedOverlay extends ItemizedOverlay<OverlayItem> {
 @Override
 public boolean onTouchEvent(MotionEvent event, MapView mapView) {
  Log.i("onTouchEvent", "GeoItemizedOverlay.onTouchEvent");
  return super.onTouchEvent(event, mapView);
 }
}

試してところ、ItemizedOverlayよりも先にMapViewへタッチイベントが伝わることが分かった。
10-26 13:29:55.577: INFO/MapDemo(4253): CustomizedMapView.onTouchEvent
10-26 13:29:55.577: INFO/MapDemo(4253): GeoItemizedOverlay.onTouchEvent

上記のクラスで言うと、

CustomizedMapView→GeoItemizedOverlay

の順番だ。


・結論

MapView側に記述してあるonTouchEventがまず呼ばれる。

そして、MapViewのonTouchEventの最後に実行する、super.onTouchEvent(event)により、MapViewに追加されている各Overlayへタッチイベントが伝えられていく、という流れのようだ。

ItemizedOverlayをMapViewに追加しているときにアイテムが無いとNullPointerExceptionが発生するバグへの対応

ItemizedOverlayがMapViewに追加されているときは、MapViewクラスのonTouchEvent(MotionEvent event)が実行される際に、ItemizedOverlayのonTouchEvent(MotionEvent event)にもイベントが投げられる。

問題は、それがアイテム数が0の時にも起こるので、アイテム数が0の時に画面にタッチするとNullPointerExceptionが発生すること。

10-26 12:00:24.607: ERROR/AndroidRuntime(2753): java.lang.NullPointerException
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.ItemizedOverlay.getItemsAtLocation(ItemizedOverlay.java:617)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.ItemizedOverlay.getItemAtLocation(ItemizedOverlay.java:586)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.ItemizedOverlay.handleMotionEvent(ItemizedOverlay.java:498)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.ItemizedOverlay.onTouchEvent(ItemizedOverlay.java:572)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at hoge.MapDemo.GeoItemizedOverlay.onTouchEvent(GeoItemizedOverlay.java:54)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.OverlayBundle.onTouchEvent(OverlayBundle.java:63)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at com.google.android.maps.MapView.onTouchEvent(MapView.java:625)
10-26 12:00:24.607: ERROR/AndroidRuntime(2753):     at hoge.MapDemo.CustomizedMapView.onTouchEvent(CustomizedMapView.java:52)

今の所この現象を避けるには、ItemizedOverlayを継承したクラスのコンストラクタで、super(boundCenterBottom(defaultMarker));の後で、populate()を呼ぶこと。


・ItemizedOverlayを使うときは、必ずコンストラクタ内でpopulate()を呼ぶ

populate()はアイテムを追加した後に必ず呼ぶメソッドだが、アイテムがあろうがなかろうが、最初からコールしておくということ。これでNullPointerExceptionは発生しなくなる。

public class GeoItemizedOverlay extends ItemizedOverlay<OverlayItem> {

 private ArrayList<OverlayItem> mOverlays = new ArrayList<OverlayItem>();
 private Context mContext;

 public GeoItemizedOverlay(Drawable defaultMarker, Context context) {
  super(boundCenterBottom(defaultMarker));
  mContext = context;
  populate();//これが大事!
 }
}

このバグはIssueとして管理されているので、早めに解決して欲しい方はvoteしてください。
Issue 2035 - android - NullPointerException when scrolling through a MapView with an ItemizedOverlay with no OverlayItems - Project Hosting on Google Code

MapView継承クラスでonTouchEvent()を処理するときに気をつけること

MapViewを利用すると、MapActivityを継承したクラス内にonTouchEvent(MotionEvent event)を書いてもイベントをキャッチできない。

以下のようなコードを書いても、MapViewの方にイベントが取られて、ログが出力されない。

public class MapDemo extends MapActivity {
 @Override
 public boolean onTouchEvent(MotionEvent event) {

  String action = "";
  switch (event.getAction()) {
  case MotionEvent.ACTION_DOWN:
   action = "ACTION_DOWN";
   break;
  case MotionEvent.ACTION_UP:
   action = "ACTION_UP";
   break;
  case MotionEvent.ACTION_MOVE:
   action = "ACTION_MOVE";
   break;
  case MotionEvent.ACTION_CANCEL:
   action = "ACTION_CANCEL";
   break;
  }

  Log.i("onTouchEvent", "action = " + action + ", " + "x = "
    + String.valueOf(event.getX()) + ", " + "y = "
    + String.valueOf(event.getY()));

  return super.onTouchEvent(event);
 }
}

そのため、MapViewクラスを継承したオリジナルのMapViewクラスを作成し、そのクラス内にonTouchEvent(MotionEvent event)を記述した。その時に気をつけること。


・MapViewクラスを継承する場合は、MapViewクラスのコンストラクタMapView(Context context, AttributeSet attrs)を必ずオーバーライドする

そうしないと、デフォルトのズームコントローラーを使ったり、フリップしてマップを動かすことができない。
静止画像のようにマップが表示されるだけの画面になってしまう。

コードは以下。

public class CustomizedMapView extends MapView {
 public CustomizedMapView(Context context, AttributeSet attrs) {
  super(context, attrs);
 }
 @Override
 public boolean onTouchEvent(MotionEvent event) {

  String action = "";
  switch (event.getAction()) {
  case MotionEvent.ACTION_DOWN:
   action = "ACTION_DOWN";
   break;
  case MotionEvent.ACTION_UP:
   action = "ACTION_UP";
   break;
  case MotionEvent.ACTION_MOVE:
   action = "ACTION_MOVE";
   break;
  case MotionEvent.ACTION_CANCEL:
   action = "ACTION_CANCEL";
   break;
  }

  Log.i("onTouchEvent", "action = " + action + ", " + "x = "
    + String.valueOf(event.getX()) + ", " + "y = "
    + String.valueOf(event.getY()));

  return super.onTouchEvent(event);
 }
}
この時のレイアウトは以下。
<?xml version="1.0" encoding="utf-8"?>
<hoge.MapDemo.CustomizedMapView
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/mapview"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:clickable="true"
 android:apiKey="apiKey" />
MapView呼び出しはこんな感じ。
public class MapDemoActivity extends MapActivity {
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  /** マップビュー作成 */
  CustomizedMapView mapView = (CustomizedMapView) findViewById(R.id.mapview);
 }
}

2010年10月22日金曜日

Buzzの日付書式のフォーマットパターン

Google Buzz APIで結果を取得したときの日付書式は、以下のもの。

2010-10-15T06:51:04.000Z

JavaのSimpleDateFormatクラスへ、この日付書式をパースするフォーマットパターンを与えると、以下になる。

yyyy-MM-dd'T'HH:mm:ss.SSS


使い方としてはこんな感じ。
private static final SimpleDateFormat FORMATTER_INPUT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
private Date published;

 public void setPublished(String published) {
  try {
   this.published = FORMATTER_INPUT.parse(published.trim());
  } catch (ParseException e) {
   throw new RuntimeException(e);
  }
 }

追記 2010.11.01
android.text.format.Timeクラスのparse3339()を使うと、もっと簡単に解析できることが分かりました。
明日に向かって昇龍拳: Buzzの日付書式のフォーマットパターンをTimeクラスのparse3339()を使って簡単にパースする

2010年3月29日月曜日

Androidアプリで現在選択されているロケールを取得する

Androidアプリで、現在選択されているロケールを取得するTips。
import java.util.Locale;

import android.util.Log;


String format = null;

// ロケールの取得
Locale locale = Locale.getDefault();
Log.v("ロケールは", locale.toString());

if (locale.equals(Locale.JAPAN)) {
 format = "yyyy年MM月dd日 HH:mm";
} else {
 format = "MMM d, yyyy 'at' HH:mm";
}

return format;
AndroidではJ2SEのAPIがそのまま使えるので、普通にutilのLocaleクラスを呼び出してロケールが判断出来ます。便利だなぁ~。

AndroidアプリでRSSフィードの日付をパースしてフォーマットする

Androidアプリで、RSSフィードの日付をパースして表示するTips。
RSSフィードのpubDateの日付書式「Mon, 29 Mar 2010 04:34:00 +0000」を任意の日付フォーマットへ変換する。
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.http.impl.cookie.DateParseException;
import org.apache.http.impl.cookie.DateUtils;

/** RSSフィードの日付フォーマットパターン */
private String pattern[] = { DateUtils.PATTERN_RFC1123 };

// ItemからpubDateを取得してフォーマット
String pubdate = item.getPubdate().toString();

String formattedPubdate = "";
try {
 Date date = DateUtils.parseDate(pubdate, pattern);
 SimpleDateFormat sdf = new SimpleDateFormat("MMM d, yyyy 'at' HH:mm");
 formattedPubdate = sdf.format(date);
} catch (DateParseException e) {
 e.printStackTrace();
}
フォーマットした文字列はViewに設定するなどして表示する。
// item_pubdateに則ってTextView生成
TextView pubdateView = (TextView) view.findViewById(R.id.item_pubdate);
pubdateView.setText(formattedPubdate);
タイムゾーンの設定により、日付がパースされる結果は動的に変わる。

タイムゾーンをGMT+09:00に設定している場合とGMT+1:00に設定している場合とでは、パース後の日付情報が異なっている。DateUtilsのparseDateメソッド、便利ですねぇ。