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();