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

2020年9月18日金曜日

Pixel3aをAndroid11へアップデートしたらモバイルネットワークが頻繁に切れる件

Android11へアップデートしたことをきっかけに、Pixel3aのモバイル通信がとんでもなく不安定になってしまった(´・ω・`)

5秒おきくらいの頻度で、繋がったり切れたりを繰り返し、ろくに調べ物もできない。


探したら似たような症状の方が。この方はAndroid10で発症。

pixel3aをAndroid10にアップデート - https://fan.uqwimax.jp/topics/detail/1155

pixel3a(simフリー版)をAndroid10にアップデートした途端に UQのAPN設定が飛びました…(´・ω・`) 手打ちで再設定して復旧しましたが pixel3aユーザーの方は御注意の程を



APNの設定を見直したら、なんと全く契約していないOCNなんかに接続設定が変わってる〜!
APN設定が初期化された?



契約先のAPNへ設定を選び直したら、復活!

Andorid9から10へアプデした際は起こらなかったが、10から11へ上げた今回は発生。
何がトリガーとなるのかは、全く不明 ┐(´~`)┌

2015年12月17日木曜日

Android Wear で Notification をすぐに表示するために必要なこと

スマホで表示される通知が Android Wear でどのように表示されるのかを確認したいのに、なぜか Wear で表示されないという状況に嵌ったので、その解決メモ。

環境

  • Handheld:Nexus 5(OS:6.0.1 Marshmallow)
  • Wearable:Sony SmartWatch 3(OS:5.1.1)
    • 通知表示:すべて
    • シアターモード:OFF

※Nexus 5 と SmartWatch 3 は接続確立済み


接続が確立されていたら、スマホ側で表示された通知は Wear 側でも表示されるはず。それがスマホでは表示されているのに Wear は無反応。


解決

原因は、Wear へ届く通知には優先順位があり、バイブする or 音を鳴らす 通知でない限りは、すぐに Wear へ通知が届かないからだった。

バイブや音を鳴らさない通知の場合、5分後など適当な間隔で Wear へ通知が届き、しれっとカードが表示されているという状態になる。それに気づかなかったので、通知が届かない〜!!とハマった。

すぐに Wear へ通知を出したい場合は、バイブや音を鳴らすように注意するべし!


以下、動作確認に使用したサンプルコード。

2015年3月14日土曜日

Android端末でとったスクリーンショットが「フォト」で同期されないと思ったら

Android端末でとったスクリーンショットが、PCのブラウザから「フォト」へアクセスしても見当たらないなと思ったら、自動バックアップがオンになっていても、スクリーンショットのディレクトリはデフォルトでは同期の対象外になっていました。

スクリーンショットのディレクトリを同期対象に含めるには以下の方法で。

Android端末で「フォト」を起動。「メニュー」から「端末内」を選択。


Screenshotsのクラウドアイコンがオフになっているので、タップしてオンに変更。


同期対象になりました。


以降は、スクリーンショットを撮る度に、PCのブラウザからGoogle+ の「フォト」へアクセスすると、すぐに画像が表示されます。

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年3月4日月曜日

MacでAndroidデバッグ用証明書を生成するときにターミナルの文字化けを防ぐには

Macでkeytoolコマンドを使ってAndroidデバッグ用の証明書を生成しようとしたら、メッセージが文字化けして何が何だか…(´・ω・`)



コマンド出力時のエンコードがUTF-8ではなく、SJISになっていることが原因。Appleが配布しているMac用のJavaの問題と思われ。


文字化けを解消するには、環境変数_JAVA_OPTIONSを指定してUTF-8のエンコードを指定する。

$_JAVA_OPTIONS='-Dfile.encoding=UTF-8' keytool -genkey -v -keystore .android/CUSTOM_debug.keystore -alias androiddebugkey -keyalg RSA -validity 10000 -dname "CN=Android Debug, O=Android,C=JP"
CUSTOM_debug.keystoreは任意の名前に置き換え





生成済みの証明書を確認する際も、文字化けを防ぐためにオプションを指定する。

$_JAVA_OPTIONS='-Dfile.encoding=UTF-8' keytool -exportcert -alias androiddebugkey -keystore .android/CUSTOM_debug.keystore -list -v

パスワードを入力すると、以下のように生成済みの証明書が表示される。





複数の開発マシーンでデバッグキーを共有するとちょっと便利

ちなみに、作成した.keystoreファイルを複数の開発マシーンで共有すると、同じデバイス宛へアプリを上書きインストールする際にも便利。

通常、同じデバイスに複数の開発マシーンからデバッグビルドでアプリを上書きインストールすると、開発マシーン毎に証明書が異なるので、アプリのアップデートができず、一度アプリをアンインストールしなければならない。

デバッグ用証明書を共有しておくと、アンインストールせずにアプリのアップデートができる。


Mac OS Xの場合、Eclipseのメニューから、「環境設定」→「Android」→「Build」→「Custom Debug keystore」に共通のデバッグ用証明書を設定すればOK。

2013年2月6日水曜日

Eclipseで設定したXMLのインデント幅がAndroidManifest.xmlに反映されないなと思ったら

Eclipseでxmlのインデントを半角スペース2つ分と設定している。



その設定をAndroidで編集するAndroidManifest.xmlなどのxmlにも反映するためには、
「Android > Editors > Use Eclipse setting for indentation width and space or tab character indentation(Android default is 4 space characters)」のチェックが必要。



ちょっと困ったのでメモ。

2013年2月4日月曜日

minSdkVersion16以上ではGCMBroadcastReceiverのintent-filterにcategory指定が要らない

4.1以上の端末と、それより下のAPIレベルの端末で、GCMサーバーからメッセージを受信した際のインテントの内容が異なることに気がついた。


4.1(APIレベル16)以上
02-04 13:25:37.552: V/GCMIntentService.onMessage:70(11555): Intent { act=com.google.android.c2dm.intent.RECEIVE flg=0x10 pkg=my.app.package cmp=my.app.package/.GCMIntentService (has extras) }


4.0.3(APIレベル15)

02-04 13:46:33.894: V/GCMIntentService.onMessage:70(380): Intent { act=com.google.android.c2dm.intent.RECEIVE cat=[my.app.package] cmp=my.app.package/.GCMIntentService (has extras) }


16以上だと、インテント内にカテゴリー指定がなく、パッケージに変わっている。

GCMのドキュメントにも、minSdkVersionを16以上に指定した場合は、マニフェストに記述するcom.google.android.gcm.GCMBroadcastReceiverのintent-filterで、categoryのname値にパッケージ名を指定する必要がないとある。

Notice that android:name in the category tag must be replaced by your application's package name (and the category tag is not required for applications targeted to minSdkVersion 16 and higher).


メッセージが送られる端末に応じて、インテントにカテゴリーを含めるのか、パッケージ名を含めるのか、振り分けているようだ。


minSDKVersionを16以上にすると、さらに、permission.C2D_MESSAGEの要求も要らなくなるし、マニフェストがスッキリするね。

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が呼ばれた。起動順序はマニフェストに記述した順序かな?

2013年1月26日土曜日

NotificationのBigPictureStyleで画像全体を表示させてみる

BigPictureStyleでは、画像の中央を中心にして、最大幅450dpに合う大きさへ画像がクロップされる。

でもそれだと、画像が縦長の場合は上下が大きくクロップされてしまい、何が写っているのかよく分からない。



画像の比率によらず、bigPicture部分で、常に画像全体が見られるようにしてみた。
private Bitmap getBigPicture(Resources res, String path) {
 Bitmap canvasBmp = null;

 final float WIDTH_DP = 450.0f;
 final float HEIGHT_DP = 192.0f;

 // ピクセル密度を取得する
 DisplayMetrics metrics = new DisplayMetrics();
 getWindowManager().getDefaultDisplay().getMetrics(metrics);
 float scaleDensity = metrics.scaledDensity;

 // ピクセル密度を加味してターゲットの幅と高さのピクセル数を計算する
 int targetWidthPx = (int) (WIDTH_DP * scaleDensity);
 int targetHeightPx = (int) (HEIGHT_DP * scaleDensity);

 // 画像読み込みオプションを作成
 BitmapFactory.Options options = new BitmapFactory.Options();
 // メモリが少なくなったらパージできるようにオプションを設定
 options.inPurgeable = true;
 // 画像サイズだけを読み込むようにオプションを設定
 options.inJustDecodeBounds = true;

 // 画像読み込み
 Bitmap srcBmp = BitmapFactory.decodeFile(path, options);

 // 画像の幅と高さのピクセル数を取得する
 int srcWidth = options.outWidth;
 int srcHeight = options.outHeight;

 // Matrixを取得する パディングは0で小さな画像は拡大しない
 Matrix mat = getMatrix(srcWidth, srcHeight, targetWidthPx, targetHeightPx, 0, false);

 // Paintを作成する
 Paint bmpPaint = new Paint();
 bmpPaint.setFilterBitmap(true);

 // ターゲット幅と高さで背景が透明のキャンバスを作成
 canvasBmp = Bitmap.createBitmap(targetWidthPx, targetHeightPx, Bitmap.Config.ARGB_8888);
 Canvas canvas = new Canvas(canvasBmp);

 // 画像サイズだけでなく画像そのものを読み込むようにオプションを設定しなおす
 options.inJustDecodeBounds = false;

 // 実際に画像を読み込み
 srcBmp = BitmapFactory.decodeFile(path, options);

 // 背景が透明のキャンバスに画像を合成する
 canvas.drawBitmap(srcBmp, mat, bmpPaint);

 // メモリ解放
 srcBmp.recycle();
 srcBmp = null;

 Log.i("#getBigPicture()", "scaleDensity=" + scaleDensity + " srcWidth="
   + srcWidth + " srcHeight=" + srcHeight + " targetWidthPx="
   + targetWidthPx + " targetHeightPx=" + targetHeightPx);
 return canvasBmp;
}

/**
 * @param sw ソース幅
 * @param sh ソース高さ
 * @param pw この幅にフィットさせる
 * @param ph この高さにフィットさせる
 * @param padding フィット時に考慮するパディング
 * @param enableMagnify 画像が小さい場合に拡大するか否か
 * @return フィットさせるためのスケール値
 */
private static final Matrix getMatrix(int sw, int sh, int pw, int ph,
  int padding, boolean enableMagnify) {
 float scale = getMaxScaleToParent(sw, sh, pw, ph, padding);
 if (!enableMagnify && scale > 1.0f) {
  scale = 1;
 }
 Log.i("#getMatrix()", "scale=" + scale);
 Matrix mat = new Matrix();
 mat.postScale(scale, scale);
 mat.postTranslate((pw - (int) (sw * scale)) / 2,
   (ph - (int) (sh * scale)) / 2);
 return mat;
}

private static final float getMaxScaleToParent(int sw, int sh, int pw,
  int ph, int padding) {
 float hScale = (float) (pw - padding) / (float) sw;
 float vScale = (float) (ph - padding) / (float) sh;
 return Math.min(hScale, vScale);
}

上記では、bigPictureの最大幅・高さのキャンバスを作り、そのキャンバスサイズに合わせて画像を縮小し、中央に配置する。

BigPictureStyleでは、450dpに合わせて、上下左右に隙間なく画像を表示するよう設計されている。しかしこの設計では、多くの場合に上下や左右に隙間ができても、常に画像全体が写るようにして、どんな画像か把握出来るようにしている。


上記メソッドを呼び出してBigPictureStyleを作るコードのスニペット。
NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(
      notificationBuilder);
bigPictureStyle.bigPicture(getBigPicture(res, path));
bigPictureStyle.setBigContentTitle(contentTitle);
bigPictureStyle.setSummaryText(contentText);
notificationBuilder.setStyle(bigPictureStyle);


結果、bigPicture部分はこんなふうに表示される。

NotificationのBigPictureStyleで表示されるbigPictureの大きさはいくつなんだ

4.1のJelly Bean以降でサポートされるようになった、リッチなNotificationスタイル。

このうちのBigPictureStyleで表示できるbigPicture(画像部分)の大きさにあわせて、元画像全体をリサイズして表示したいと思い、サイズを調べてみた。


・高さは192dp

Android Developersのサイトでは、高さの上限は最大256dpと書いてある。
Through an improved notification builder, apps can create notifications that use a larger area, up to 256 dp in height.

同じ内容がGoogle I/O 2012のセッション(32分40秒あたり)でも触れられている。そのスライドでは、BigContentView全体で、256dpが最大の高さと示してある。



つまり、256dpはbigPictureの高さではなく、BigPictureStyleのNotification全体での高さ上限だということ。よって、bigPictureの高さはNotificationを広げた256dp(4U)の状態から、Notificationを広げていない64dp(1U)の状態を引いた値になる。

256dp(4U) - 64dp(1U) = 192dp(3U)


・幅は450dp

幅は、同じく上記のGoogle I/O 2012のセッション(36分30秒あたり)で、最大450dpだとある。



まとめるとこんな感じ



・pxだとどのくらい?

例えば、Nexus 7はtvdpiなので、ピクセル密度(density)は1.33125。

450dp x 1.33125 = 約599px
192dp x 1.33125 = 約255px

bigPicture部分には、幅599px、高さ255px程度の大きさの画像が表示される。

2012年10月30日火曜日

Nexus 7(32GB)の日本での販売開始。Nexus 10は11月13日から。Nexus 4は日本販売予定なし。

Nexus 4, 7, 10が発表された。ハリケーン「サンディ」上陸の影響で発表会が開けなくて、Googleは残念だったろう。

以下は発表された販売スケジュール。ちなみに、Nexus 4とNexus 7(32GB)3G対応モデルの、日本での販売予定は今のところ無い



どちらもOSは4.2(Jelly Bean)。

8GBは$299、16GBは$349。米国、英国、オーストラリア、フランス、ドイツ、スペイン、カナダのGoogle Play Storeで11月13日から販売開始。

Nexus 4はLTEが使えないのがミソ。



OSは4.1(Jelly Bean)。

16GBのWiFiモデルは、既に日本でも販売されているが、今日から32GB WiFiモデルの販売も始まった。価格は24,800円


Nexus 7(32GB)3G対応モデル by ASUS

OSは4.1(Jelly Bean)。

$299でアンロックされて販売される。米国、英国、オーストラリア、フランス、ドイツ、スペイン、カナダのGoogle Play Storeで11月13日から販売開始。



どちらもOSは4.2(Jelly Bean)。

16GBは36,800円、32GBは44,800円で、11月13日から日本で販売開始。その他、米国、英国、オーストラリア、フランス、ドイツ、スペイン、カナダのGoogle Play Storeでも11月13日から販売開始。

Android 4.2(Jelly Bean)の特徴まとめ

Android 4.2搭載のNexus 4, 7, 10が発表された。そこで、4.2で新しく加わった機能などをを少しまとめてみた。


・コードネームはJelly Beanのまま

4.1から変わらないのね。


・Photo SphereでJPEG画像がストリートビューのように見られる

今回のOSで一番売りにしているポイントっぽい。

JPEG画像のメタデータの中にXMLを含めることで、静止画像なのに上下左右など、撮影した画像の周辺が360度見られる。
Hugo Barra - Google+ - Photo Sphere: capturing the world around you We just…

4.2でJPEG画像を開けば、なんでもそのように見られるわけではなく、4.2搭載の端末のカメラで撮影した画像のみ対応する。


・ユーザーの切り替えができるようになった(タブレット限定機能)

MacやWindowsなどのデスクトップOSのように、マルチユーザーで1端末が使用される、ホームユースを考えて搭載した機能だろうね。

ユーザー毎に領域が分けられているので、ホームスクリーン、壁紙、ウィジェット、アプリ、ゲームのハイスコアなども、個人ごとに管理される。ログアウトせずに、別のユーザーに切り替えることも可能。

これはタブレット限定の機能で、4.2搭載でもフォンタイプではこの機能は使えないみたい。


・フォンからタブレットへ動画や写真をワイアレスで映し出す

動画や画像をもうちょっと大きく見たいという時に、HDMI対応のTVや4.2搭載タブレットへダイレクトで表示内容を映し出すことが出来る。

家族が集った時や結婚式などのイベントで有効に使えるかも?


その他の情報は Android - What's New で確認できる。

2012年10月27日土曜日

GCMのIntentServiceでAsyncTaskを実行したらsending message to a Handler on a dead threadの警告が出て悩まされる

GCMサーバーからメッセージが送られてきた時の処理をGCMIntentServiceのonMessage()へ記述している。
my.app.GCMIntentService
  |- com.google.android.gcm.GCMBaseIntentService
      |- android.app.IntentService

onMessage()内でAsyncTaskクラスを作成してexecute()したところ、以下のようなWarningが出た。
10-26 15:24:14.025: W/MessageQueue(482): Handler{44f63ea8} sending message to a Handler on a dead thread
10-26 15:24:14.025: W/MessageQueue(482): java.lang.RuntimeException: Handler{44f63ea8} sending message to a Handler on a dead thread
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.MessageQueue.enqueueMessage(MessageQueue.java:179)
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.Handler.sendMessageAtTime(Handler.java:457)
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.Handler.sendMessageDelayed(Handler.java:430)
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.Handler.sendMessage(Handler.java:367)
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.Message.sendToTarget(Message.java:348)
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.AsyncTask$3.done(AsyncTask.java:214)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.FutureTask$Sync.innerSet(FutureTask.java:252)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.FutureTask.set(FutureTask.java:112)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:310)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.FutureTask.run(FutureTask.java:137)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1068)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:561)
10-26 15:24:14.025: W/MessageQueue(482):  at java.lang.Thread.run(Thread.java:1096)

コードのスニペット。
@Override
 protected void onMessage(final Context context, Intent intent) {
  Log.i(TAG, "Received message.");
  // IMAGE_IDを取得
  String imgId = intent.getStringExtra(IMAGE_ID);
  
  // Async version
   GetImgTaskTest getImgTaskTest = new
   GetImgTaskTest(context.getApplicationContext());
   getImgTaskTest.execute(imgId, null, null);
 }


StackOverFlowで、このWarningの回避策が触れられていた。

Issue 20915 - android - AsyncTask can get initialized with wrong Looper - Android - An Open Handset Alliance Project - Google Project Hosting

そこでは、onCreate()内でAsyncTaskクラスをロードしておくやり方が紹介されていた。
public class YourApplication extends Application {

    @Override
    public void onCreate() {

        // workaround for http://code.google.com/p/android/issues/detail?id=20915
        try {
            Class.forName("android.os.AsyncTask");
        } catch (ClassNotFoundException e) {
        }

        super.onCreate();
    }
}


これはAsyncTaskクラスのバグだという言及も。

android - onPostExecute not being called in AsyncTask (Handler runtime exception) - Stack Overflow

This is due to a bug in AsyncTask in the Android framework. AsyncTask.java has the following code:

private static final InternalHandler sHandler = new InternalHandler();

It expects this to be initialized on the main thread, but that is not guaranteed since it will be initialized on whichever thread happens to cause the class to run its static initializers. I reproduced this issue where the Handler references a worker thread.


しかし、このClass.forName()を使う方法は、あまりエレガントに思えなかったので、AsyncTaskのスレッドルールについて調べてみた。

AsyncTask | Android Developers "Threading rules" より
・The AsyncTask class must be loaded on the UI thread. This is done automatically as of JELLY_BEAN.
・execute(Params...) must be invoked on the UI thread.

AsyncTaskのAPIドキュメントにも、UI threadでクラスをロードせよ、execute()はUI threadで実行せよと、しっかり書いてある。


ナゼ?


まず基本的なことから。


・AndroidのUIは基本的にsingle threadモデルであることを理解すべし

AndroidのUIは基本的にsingle threadモデル。全てのアプリは1つのプロセスとスレッドで実行される。そのスレッドのことを別名"main" threadとよんでいる。

UI threadの中で、ネットワーク通信やデータベース検索など、時間がかかる処理を実行した場合、レスポンスが悪くなったり、アプリが止まっているように見えたりしてしまう。よって、以下の2点だけは守ろう。

Processes and Threads | Android Developers "Threads"より
Thus, there are simply two rules to Android's single thread model:

1. Do not block the UI thread
2. Do not access the Android UI toolkit from outside the UI thread


・時間がかかる処理はworker threadで行い、main threadから分けるべし

即座に応答しなくてよい操作だったら、それらは別のスレッド(バックグラウンドやworker threads)で処理できないか確認しよう。

Processes and Threads | Android Developers "Worker threads"より
If you have operations to perform that are not instantaneous, you should make sure to do them in separate threads ("background" or "worker" threads).

で、worker threadの結果は、以下の様なメソッドやクラスを使って反映させよう。



分かった!Warningの理由

そう、つまり、AsyncTaskは、worker threadがUI threadとコミュニケートしながら処理を進めるという操作を、簡単に書けるように作られたクラスなのだ。だから、execute()はUI thread内で実行することを想定している。

AsyncTaskのClass Overviewにも、真っ先に書いてある。

「AsyncTaskはUI threadを適切に、そして簡単に使えるようにしたもの。このクラスを使うと、ThreadやHandlerを使って自分で色々せずに、処理をバックグラウンドで行い、その結果をUI threadへ表示させることができる。」
AsyncTask enables proper and easy use of the UI thread. This class allows to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.

An asynchronous task is defined by a computation that runs on a background thread and whose result is published on the UI thread.


ここまで整理すると、Warningが出ていた理由も理解できる。

UI threadで実行するルールになっているAsyncTaskを、IntentService内で、つまりworker thread内でexecute()したていたからだ。

IntentSreviceではAsyncTaskは使えない。

非同期処理と思うとAsyncTaskと直ぐに思ってしまうけれど、常に使える手ではないということだろう。


じゃ、どうするか。


そもそもやりたかった処理は、メッセージを受信したら画像をダウンロードするというもの。その際Activityは起動しない、Notificationのみ表示する。onMessage()はその処理をキックするだけ。

ムム〜。やっぱりThraed回すのが手堅そう。となると、java.uti.concurrent.Executor あたりにヒントがありそう。

2012年8月10日金曜日

GCMの実装でおさえておきたいポイント

前回の記事「Google Cloud Messaging(GCM)とC2DMの主な違いを挙げてみた」を書いていて、覚えておきたいなと思ったポイントがいくつかあったのでメモメモ。


同一メッセージには同一Collapse Keyを指定する

デバイスがオフラインからオンラインへ復帰した時、GCM Serverはストレージされたメッセージをデバイスへ再送する。

たまたま通信状態が悪かったりして、メッセージが届かないと思ったユーザーが、Client Application Serverから同じメッセージを何回もGCM Serverへ送っていたら、同じようなメッセージをいくつもデバイスで受信することになる。

Collapse Keyを同じにしていると、GCM Server側で、全てのメッセージではなく最新のメッセージ1つだけを選んで送る。

Collapse Keyは適当にランダムなどではなく、同一メッセージで統一させた方がいいなあ。


「Device not Registered」エラーが出たら、Client Application ServerからRegistration IDを削除する

デバイス側でGCMを使ったAndroid Applicationが削除された場合、次回メッセージ送信時に「Device not Registered」エラーが出る。

このとき、Client Application Server側で、非有効なRegistration IDをクリーンアップした方がいいなあ。


デバイスのバッテリーを節約するには「delay_while_idle」の指定が効きそう

「delay_while_idle」はC2DM時代からあるパラメータ。デバイスがオンラインでも、unlockされてデバイスの操作が始まらない限りメッセージを送らない、という指定ができる。

メッセージの即時性は無くなるけれど、デバイスが起きているときにだけメッセージが送られるので節電効果があるなあ。

2012年8月9日木曜日

Google Cloud Messaging(GCM)とC2DMの主な違いを挙げてみた

Google I/Oのプレゼン動画から、Google Cloud Messaging(GCM)の特徴やC2DMとの主な違いを挙げてみた。

C2DM時代は・・・
  • 開発者が利用するにはフォームの入力が必要でアクティベーションのメールを受信するという流れ
  • Sender Auth Tokenの取得にClient Loginを使用
  • 一日あたり20万メッセージまでというクオータの制限があった

GCMでは・・・
  • Google APIs Consoleにプロジェクトを追加してGCMを有効にするだけで利用できるようになる
  • 脱Client Login
  • クオータ制限なし

GCMでも変わらないこと
  • 無料のサービス
  • Froyo以上で稼働
  • メッセージ受信にRegistration IDが必要
  • メッセージがデバイスへ届くまでの基本的な流れ(Client Application Server→GCM Server→Android Device)

GCMで変わったこと
  • Project IDが必要
    Sender IDの代わり。Google APIs Consoleでプロジェクトを追加した時に取得。
  • API Keyが必要
    Sender Auth Tokenの代わり。Google APIs Consoleのプロジェクト管理画面で取得。
  • GCM Serverがjson形式のデータにも対応。json形式でリクエストを受け付けたりレスポンスを返したりするようになった。

GCMで追加・改善された機能

1.Message Multicasting
メッセージを複数デバイスへ一斉送信できるようになった。最大1,000台まで。

2.Expiring Messages
メッセージを再送する期間を指定できるようになった。

「time_to_live」パラメータにメッセージが有効な期間を秒単位で設定。0秒から4週間(デフォルト)まで指定可能。0秒の時はGCM Serverにメッセージは保存されずに直ちにデバイスへ送られる。この時デバイスがオフラインだったらメッセージは捨てられる。

指定した値をオーバーした段階でGCM Serverはメッセージをクリーンアップする。

3.Messages with Payload
メッセージにデータを含められるようになった。1メッセージあたり最大4KBまで。主に、チャットなどの簡単なテキスト送信に利用できる。

オフライン時間が長くてメッセージがたくさん溜まってしまっていたら、オンラインへ復帰した時に、GCM Serverは全てのメッセージを削除して「たくさんメッセージが溜まっていますよ」という1メッセージだけをデバイスへ送る。削除されたメッセージ数はGCM Serverからのレスポンスに含まれている。

デバイスがオフライン時にGCM Serverへ保存しておけるのは、最大100メッセージまで。

4.Speed
メッセージ送信がめっちゃ速くなった。GCM Serverへリクエストが届いてからメッセージを送り出すまで4.7ミリ秒。

5.Multiple Senders for Social Updates
1つのAndroid Applicationで複数のプロジェクトのメッセージを受け取れるようになった。自社サービスのメッセージも、Google Plusなどソーシャルサービスなどのメッセージも。ソーシャルサービスのProject IDをどうやって取得するのか、そのフローがよくわからない(´・ω・`)。

最大100個までのProject IDが指定可能。


補足1:GCMの信頼性はいかほど?
Reliable Message Queue(RMQ)の働き

RMQは、GCM Serverとデバイス側のGCM Frameworkとの間で動作するもの。メッセージが確実にデバイスへ届いたのかチェックする。

例えば、GCM Serverがメッセージを送った時、デバイスの接続が切れてしまい、メッセージが届かなかったとしても、接続が復帰してデバイスがGCM Serverへアクセスした際に、メッセージが届いていないことをRMQが探知する。そして、GCM Serverはメッセージを再送する。

ACKを送り合う事で到達の確認をしている。いちいちではなく、ある程度のまとまりでACKを送っている。

このような仕組で信頼性が高いシステムになっているのがGCMの強み。GCMはメッセージを無くさない( ̄^ ̄)


補足2:Android Developer ConsoleでGCM Serverが処理したメッセージの統計が見られるようになった

そのアプリでGCMへ登録された数や、OSバージョンなどの端末情報、メッセージが送られた数やCollapseされた数、GCM Serverからのエラーメッセージなどが見られる。


補足3:ICS以上からGoogle Accountをデバイスへ登録する必要が無くなった

ICS以上から、デバイスへGoogle Accountを事前登録する事無くプッシュサービスが使えるようになったとな。本当かな?こんど検証してみようっと。


おまけ:デバイスからメッセージを送れるようになる?

その機能を含んだAPIはあるけれど開発者向けには公開していない、と。それが出来るようになると、またパラダイムが変わりそう。期待したい。


超おまけ:スピーカーのFrancescoがキュートやわ〜(*^_^*)

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年1月19日木曜日

C2DMを使ったアプリケーションを作るために必要なもの、必要なこと

必要なもの
  • Google C2DM Server
  • Application Server
  • Android Application
  • Client Application

必要なこと

[Google C2DM Server]
登録に必要なもの
  1. 開発者の連絡先メールアドレス
  2. Sender ID(C2DMサーバーへデータを送信する際に利用されるメールアドレス)

    Sender ID毎に利用できるクオータ値(一日あたり20万メッセージまで)が割り振られる。なので、Sender IDはグローバルなアドレスを設定するのではなく、C2DMを使うアプリケーション毎に個別に設定した方がよさそう。
  3. Android Applicationのパッケージ名(e.g. com.example.xxx)
登録後に、Sender Auth Token を取得する

[Application Server]
  • Client Applicationをデプロイする

[Android Application]
  • C2DMを使ったアプリを開発する

    Android OS 2.2(Froyo)以上が対象

    最低1台の端末にはGoogle Accountの設定が必要
アプリのマニフェストには以下の情報を必ず埋め込む
  1. Sender ID
  2. Android Application のパッケージ名(e.g. com.example.xxx)
  3. アプリはGoogle C2DM サーバーからデータを受信するためにBroadcast Receiver を実装して作る
  • アプリからGoogle のC2DMサーバーへリクエストを送りディバイスを登録する

    レスポンスからRegistration ID を取得する

    Registration IDをアプリ内に保存する

[Client Application]
  • Google のC2DMサーバーへリクエストを送るアプリケーションを開発する
そのアプリケーションでは、HTTPS通信で、以下の情報をC2DMサーバーへ送る
  1. Sender Auth Token
  2. Registration ID
  3. データ※1024バイト以下