2013/09/17

Android:ActionBar下部にProgressBarを表示


上図のようなActionBarの直下にプログレスバーを表示するUIがあります。
ActionBarの機能かと思ったのですが、どうもそうではなさそうなので自作しました。
# 類似のPJにPullToRefreshがあります

まずはActionBar下部に表示される影を消します。
これがあると、ActionBarとプログレスバーの間に隙間ができてしまいます。
<activity
    android:name="yuki312.android.actionbarprogress.MainActivity"
    android:label="@string/app_name"
    android:windowContentOverlay="@null" >

次にプログレスバーを配置するためのレイアウトを定義。
画面最上部にプログレスバーを配置します。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="0dp" >
    <ProgressBar
        android:id="@+id/view_actionbar_progress"
        android:layout_width="match_parent"
        android:layout_height="3dp"
        android:indeterminateDrawable="@drawable/ic_progress_indeterminate" />
</RelativeLayout>

indeterminateDrawableにプログレスバーの画像を指定します。
プログレスバーの画像はアニメーションさせるのが一般的です。
1コマ毎の画像を用意してアニメーションリソースを用意しても良いのですが、
リソース節約のため、ここではShapeで描画します。
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false" >
    <item android:duration="50">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="1.0"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_light"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>
    <item android:duration="50">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="0.8"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_light"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>
    <item android:duration="50">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="0.6"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_light"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>
    <item android:duration="50">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="0.4"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_light"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>
    <item android:duration="50">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="0.2"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_light"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>
    <item android:duration="70">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="0.0"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_normal"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>
   
    <item android:duration="100">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="0.2"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_normal"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>
    <item android:duration="100">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="0.4"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_normal"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>
    <item android:duration="100">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="0.6"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_normal"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>
    <item android:duration="100">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="0.8"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_normal"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>
    <item android:duration="100">
        <shape android:shape="rectangle" >
            <gradient
                android:angle="180"
                android:centerY="1.0"
                android:endColor="@color/progress_dark"
                android:centerColor="@color/progress_normal"
                android:startColor="@color/progress_dark"
                android:type="linear" />
        </shape>
    </item>

</animation-list>
これで、ActionBar下部にプログレスバーが表示されます。

ソース+apkをgithubにアップしています。
https://github.com/YukiMatsumura/ActionBarProgress

以上です。

2013/05/18

Android:AlermManagerとネットワークアクセスを組み合わせる際のマナー


AlermManagerを使用すれば、任意の時間に処理を実行することができます。
Android Developers : AlermManager.set (int, long, PendingIntent)

時間指定の方法には次のタイプがあります。
  • 絶対時間で指定するRTCタイプ
  • 相対時間で指定するELAPSED_REALTIMEタイプ
○○時○○分に処理したい場合はRTC、今から○○分後に処理したい場合はELAPSED_REALTIMEといった具合です。

どちらのタイプを使用するかはアプリ要件に左右されますが、もしあなたのアプリケーションが
 タイマー発火を契機にネットワークアクセスする仕様、かつRTCタイプを採用しようとしている
のであれば一度次のことを検討してみましょう。
この処理/仕様は、相対時間指定(○○分後)で実現可能か。あるいはそのように処理/仕様を変更できないか。
もし、この問いに"YES"であるならRTCタイプではなくELAPSED_REALTIMEタイプを採用するのがよい選択です。

これには次の理由があります。
絶対時間指定の場合、アプリからのネットワークアクセスが集中する
数百万(あるいは数千万!)DLされたアプリが、特定の時間に一斉にネットワークアクセスを開始すればどうなるかは容易に想像できますね。

ネットワークアクセスのタイミングをRTCタイプでしか実現できないのであれば、
「端末によって絶対時間指定の時刻をずらせないか」
を検討するのも良い案です。
ネットワークアクセス時刻を設定/初期化する際に、分散の大きい乱数でも加えてあげればネットワークアクセスも分散されます。

重要なのは"ネットワークアクセスを集中させないこと"です。
ELAPSED_REALTIMEタイプを採用したからといって"AM8:00"までの時間差分を指定しては問題の解決にはなりません。

この手の話題は"アプリのマナー"に関わる部分です。
マナーを守るためにはソースコードの修正が必要かもしれません。
もしかすると、あなたのアプリのパフォーマンスがほんのちょっぴり低下するかもしれません。
適用したからといって、ユーザレビューの評価点にはそれほど影響しないでしょう。
しかし、こういったマナーを守るアプリが増えれば、きっとその反対よりは良い世の中になりそうです。
プロとして"気の利いたアプリ"を作っていきたいものですね。

以上です。
2013/05/15

Android:商用署名(キーストア)をデバッグ用に変更する手順

チームでAndroidアプリ開発をする上で
 「複数人で開発する際に、デバッグキーを共有したい
という方は多いでしょう。

また、セキュリティ上危険ではありますが、
 「商用署名を一時的にデバッグキー(デバッグ署名)として運用したい
なんてこともあるかもしれません。

これらを、Eclipse/ADTのデバッグ機構で実現するには少しコツが入ります。

本稿では、Eclipse/ADTで商用署名(キーストア)をデバッグキーとして運用するために必要な
  商用署名(キーストア)をデバッグ用に変更する手順
についてまとめました。

一般的に、商用署名は厳重に管理すべきものであり、無闇に複製/編集すべきものではありません。
商用署名を直接編集するなら、破損しても復旧できるようにバックアップは必ず取りましょう。
本稿で使用するパスワードはテスト用です。本番環境ではセキュアなパスワードを指定してください。

今回の検証環境
---
 OS: WindowsXP
 Eclipse: 3.7.1(Indigo)
 ADT: 21.1
---

検証で使用するキーストア情報。
---
 キーストアファイル名
   yuki.keystore
 キーストアパスワード
   yukipass
 エイリアス
   yukialias
 エイリアスパスワード
   yukialiaspass
---

●Eclipseにデバッグキーを設定する方法

下記の項目から設定します。
[Window]>[Preferences]>[Android]>[Build]

図の①に、使用するデバッグ用キーストアファイルのパスを指定します。
ここに予め作成しておいたyuki.keystoreを指定すると...
次のエラーで弾かれてしまいます。

エラー:Keystore was tampered with, or password was incorrect


「キーストアが改ざん、あるいはパスワードが正しくない」旨のエラーです。
Eclipse/ADTで使用できるデバッグキー(キーストア)情報は下記の条件を満たす必要があります。
---
 キーストアファイル名
   任意
 キーストアパスワード
   android
 エイリアス
   androiddebugkey
 エイリアスパスワード
   android
---

つまり、デバッグキーとして使用するキーストアパスワードは
  android
である必要があるのです。
yuki.keystoreのキーストアパスワードは現在"yukipass"であるため、デバッグキーとして使用できません。

それでは、yuki.keystoreをデバッグキーとして使用するためにキーストア情報を変更します。


●キーストアのパスワードを変更する方法

$keytool -storepasswd -keystore yuki.keystore
キーストアのパスワードを入力してください: <現在のキーストアパスワード>
新規 keystore password: <新しいキーストアパスワード> # ★ android を指定
新規 keystore password を再入力してください: <新しいキーストアパスワードを再入力>
Windowsなら下記を実行(JDK_HOMEのパスは自環境用に読み替えて下さい)
C:\Program Files\Java\jdk1.6.0_12\bin>keytool.exe -storepasswd -keystore yuki.keystore
このキーストアをEclipseに設定してもダメです。
次のエラーで弾かれます。

エラー:Unable to find debug key in keystore!

「キーストア内にデバッグキーが見つからない」旨のエラーです。
デバッグキーとして使用するエイリアスは
  androiddebugkey
である必要があります。


●エイリアスを変更する方法

$keytool -changealias -alias yukialias -keystore yuki.keystore
destination の別名を入力してください: <新しいエイリアス> # ★ androiddebugkey を指定
キーストアのパスワードを入力してください: <現在のキーストアパスワードを入力> # 既に android に変更済なら android で
<yukialias> の鍵パスワードを入力してください。 <現在のエイリアスパスワードを入力>
Windowsなら下記を実行。
C:\Program Files\Java\jdk1.6.0_12\bin>keytool.exe -changealias -alias yukialias -keystore yuki.keystore
このキーストアをEclipseに設定してもまだダメです。
↓のエラーで弾かれます。

エラー:Cannot recover key

「キーを復元できない」旨のエラーです。
デバッグキーとして使用するエイリアスパスワードもやはり
  android
である必要があります。


●エイリアスパスワードを変更する方法

keytool -keypasswd -alias androiddebugkey -keystore yuki.keystore
キーストアのパスワードを入力してください:
<androiddebugkey> の鍵パスワードを入力してください。 <現在のエイリアスパスワード>
新規 <androiddebugkey> の鍵のパスワード: <新しいエイリアスパスワード>  # ★ android を指定
新規 <androiddebugkey> の鍵のパスワード を再入力してください:<新しいエイリアスパスワードを再入力>
Windowsなら下記を実行。
C:\Program Files\Java\jdk1.6.0_12\bin>keytool.exe -keypasswd -alias androiddebugkey -keystore yuki.keystore
このファイルをEclipseに指定すれば見事デバッグキーとして使用できます。


●おわりに

本稿では、わざと数回に分けてキーストア情報を変更しましたが、keytoolのオプションを工夫すればコマンドをまとめることができます。
keytoolコマンドに限りませんが、(テスト目的を除き)安全でないシステムで実行する場合は、コマンドやスクリプトにパスワード情報を含めてはいけません。コマンドやスクリプトはヒストリとして蓄積され、パスワードが漏洩する可能性があります。
オプションで必要なパスワードを指定しなかった場合は、パスワードの入力を求められるのでそこで入力するようにします。

参考:
keytool - 鍵と証明書の管理ツール
Android Developers - アプリケーションへの署名

以上です。

2013/05/13

Android:Notification領域のmoreアイコン

Notificationアイコンが通知領域に収まらない時、オーバフローを表現するために"more icon"が表示されます。

しかし、Android標準の不具合によりこの機能はうまく動作しません。
#各キャリア端末ではこの不具合は改修されて再現しませんが、Nexus7等の標準機であれば確認できます。

エミュレータで動作確認する際に"more icon"が表示されない場合はこの不具合である可能性があります。
不具合のfixソースはandroid.gitへ既にコミットされています。

commit cd231432ff16cb35aa08cd7b9ca801d26bef261f
"[+&gt;" more icon was never shown in PhoneStatusBar

"[+&gt;" more icon was never show in status bar because
the member variable for this icon was not initialized
from resources. This fix enables "[+&gt;" icon to appear
in status bar when the number of indications in status
bar becomes large.
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -415,6 +415,7 @@
     mSystemIconArea = (LinearLayout) mStatusBarView.findViewById(R.id.system_icon_area);
     mStatusIcons = (LinearLayout)mStatusBarView.findViewById(R.id.statusIcons);
     mNotificationIcons = (IconMerger)mStatusBarView.findViewById(R.id.notificationIcons);
+    mMoreIcon = mStatusBarView.findViewById(R.id.moreIcon);
     mNotificationIcons.setOverflowIndicator(mMoreIcon);
     mStatusBarContents = (LinearLayout)mStatusBarView.findViewById(R.id.status_bar_contents);
     mTickerView = mStatusBarView.findViewById(R.id.ticker);
from android.git

以上です。
2013/05/07

Android:toLowerCase/toUpperCaseに注意

最近のLintは賢くなって、String.toLowerCaseやtoUpperCaseをLocale引数無しで使用すると警告を出すようになりました。
Locale指定がないと、予期せぬ事態を招く可能性があるからです。
本稿はLocaleについて少し触れてみたいと思います。

突然ですが、下記の評価式はtrue or falseどちらを返すでしょうか。
"YUKI".toLowerCase().equals("yuki")
答えは true 、といきたいところですがfalseにもなり得るのです。

評価結果がfalseとなるのは、たとえばデバイスの言語設定がトルコ語であった場合です。


トルコ語では"I"の小文字は"i"ではありません。
そのため、"YUKI"を小文字化しても"yuki"にはならないためfalseと評価されます。
つまり、Localeを指定しないString型の文字列比較では、言語設定(Locale)によって評価結果が変化する可能性があるということです。
これを回避するには、ロケールに依存しないようにtoLowerCase(Locale.ENGLISH)を使用します。

String.toLowerCase(Locale)リファレンス
http://docs.oracle.com/javase/6/docs/api/java/lang/String.html#toLowerCase(java.util.Locale)


それでは、下記の評価式はtrue or falseどちらを返すでしょうか。
"YUKI".equalsIgnoreCase("yuki")
答えは、ロケールに関係なく true が返されます。

先述のtoLowerCaseの件を考えると、equalsIgnoreCase(String)もロケールに依存するように思えます。
しかし、equalsIgnoreCaseの評価はロケールに依存しないのです。

これは、equalsIgnoreCaseで用いられるのは、String.toLowerCase(), String.toUpperCase()ではなく、
Character.toLowerCase(char), Character.toUpperCase(char)であるためです。

Characterクラスが持つそれぞれのメソッドはロケールに依存したケースマッピングをサポートしていません。
そのため、言語設定がトルコ語になっても評価に影響することがないのです。

String.equalsIgnoreCase(String)リファレンス
http://docs.oracle.com/javase/6/docs/api/java/lang/String.html#equalsIgnoreCase(java.lang.String)


i18nの敷居の高さが伺える事例でした。
ただ、下記のような状況があり得るというのには多少違和感を感じますね。
 "YUKI".toLowerCase().equals("yuki")=false
 "YUKI".equalsIgnoreCase("yuki")=true

その他参考:
Local:
http://developer.android.com/reference/java/util/Locale.html#default_locale

以上です。
2013/02/18

Android:Service.dumpでサービスの状態をダンプする

Serviceにはdumpメソッドが用意されています。
これは下記コマンドが実行された時に、サービスの状態をダンプするメソッドです。
$ adb shell dumpsys activity service <yourservicename>
Service.dump
http://developer.android.com/reference/android/app/Service.html#dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[])

dumpメソッドにはPrintWriterインスタンスが引数として渡されます。
これにサービスの情報を入力すれば、ログとしてそれが出力されます。

・動作サンプル
$ adb shell dumpsys activity service yuki.test.messenger/.MyMessenger
MyMessenger.java
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] options) {
    pw.println("MyMessenger dump!!!");
}
-出力結果-
SERVICE yuki.test.messenger/.MyMessenger 41fa2ef0 pid=6151
  Client:
    MyMessenger dump!

Service.dumpメソッドを提供することで、開発者はサービスの情報を好きなタイミングで取得することが可能です。
※ただし、対象のサービスが実行中であることが前提です
ActivityManagerServiceもこのメソッドを提供しています。
ActivityManagerServiceをdumpすればデバッグ時に有益な情報を確認できます。
http://yuki312.blogspot.jp/2012/04/androidactivity.html

Object.toStringのそれと同じく、Service.dumpは非常に有益なメソッドです。
toStringをオーバライドする際の一般契約と同じように、dumpする内容は"簡素であり、読みやすく、有益な表現"であるべきでしょう。


●サンプルコード

Service.dumpの第三引数 options にはコマンド引数が格納されます。
$ adb shell dumpsys activity service yuki.test.messenger/.MyMessenger -a -b -c
であれば
 options[0] = "-a"
 options[1] = "-b"
 options[2] = "-c"
といった具合です。

これらを活用すれば、必要に応じたダンプ情報を得ることができます。
また、よくある"ヘルプコマンド"も実装可能です。

下記はそのサンプルコードです。
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] options) {
    boolean dumpAll = false;
    boolean dumpClient = false;
    boolean dumpMessage = false;
    boolean dumpState = false;

    int optionIndex = 0;
    while (optionIndex < options.length) {
        String option = options[optionIndex];
        if (TextUtils.isEmpty(option) || option.charAt(0) != '-') {
            break;
        }
        optionIndex++;

        if ("-a".equals(option) || "-all".equals(option)) {
            dumpAll = true;
        } else if ("-c".equals(option) || "-client".equals(option)) {
            dumpClient = true;
        } else if ("-m".equals(option) || "-message".equals(option)) {
            dumpMessage = true;
        } else if ("-s".equals(option) || "-state".equals(option)) {
            dumpState = true;
        }  else if ("-h".equals(option) || "-help".equals(option)) {
            pw.println("MyMessenger dump options:");
            pw.println("  [-a] [-c] [-m] [-s]");
            pw.println("    a[ll]: dump all info.");
            pw.println("    c[lient]: reply target.");
            pw.println("    m[essage]: handle message log.");
            pw.println("    s[tate]: service state change log.");
            return;
        } else {
            pw.println("Unkown argument: " + option + "; use -h for help.");
        }
    }

    pw.println("Service state");
    pw.println("    Service create time is " + new Date(mOnCreateTime));
    pw.println("    " + mCurrentState);
    pw.println();

    if (dumpClient || dumpAll) {
        pw.println("Client(" + mClients.size() + ")");
        for (ClientRecord cr : mClients.keySet()) {
            pw.println("    " + cr);
        }
        pw.println();
    }

    if (dumpMessage || dumpAll) {
        pw.println("Message Logs");
        for (MessageLog log: mMessageLogs) {
            pw.println("    " + log);
        }
        pw.println();
    }

    if (dumpState || dumpAll) {
        pw.println("ServiceState Logs");
        for (ServiceState.ServiceStateLog log: mCurrentState.getStateLogs()) {
            pw.println("    " + log);
        }
        pw.println();
    }
}
-出力結果-
・引数無し
$ adb shell dumpsys activity service yuki.test.messenger/.MyMessenger
SERVICE yuki.test.messenger/.MyMessenger 41fd0950 pid=6151
  Client:
    Service state
        Service create time is Sun Jan 06 03:58:13 GMT+00:00 1980
        Current service state is RUNNING
・引数指定(-c:クライアント情報出力, -m:メッセージ履歴出力)
$ adb shell dumpsys activity service yuki.test.messenger/.MyMessenger -c -m
SERVICE yuki.test.messenger/.MyMessenger 41fa2ef0 pid=6151
  Client:
    Service state
        Service create time is Sun Jan 06 03:58:28 GMT+00:00 1980
        Current service state is BLOCKED

    Client(2)
        ClientRecord [ PackageName=test.pkg1, ID=1, hash=b6f4c7b1 ]
        ClientRecord [ PackageName=test.pkg2, ID=1, hash=b6f4c7b2 ]

    Message Logs
        03:58:44.570: { what=3 when=-1ms }
        03:58:44.339: { what=2 when=-1ms }
        03:58:28.681: { what=1 when=-5ms }
・引数指定(-a:全情報出力)
$ adb shell dumpsys activity service yuki.test.messenger/.MyMessenger -a
SERVICE yuki.test.messenger/.MyMessenger 41fa2ef0 pid=6151
  Client:
    Service state
        Service create time is Sun Jan 06 03:58:28 GMT+00:00 1980
        Current service state is BLOCKED

    Client(2)
        ClientRecord [ PackageName=test.pkg1, ID=1, hash=b6f4c7b1 ]
        ClientRecord [ PackageName=test.pkg2, ID=1, hash=b6f4c7b2 ]

    Message Logs
        03:58:44.570: { what=3 when=-1ms }
        03:58:44.339: { what=2 when=-1ms }
        03:58:28.681: { what=1 when=-5ms }

    ServiceState Logs
        03:58:44.571: { RUNNING => BLOCKED }
        03:58:44.340: { NEW => RUNNING }
・引数指定(-h:ヘルプ)
$ adb shell dumpsys activity service yuki.test.messenger/.MyMessenger -h
SERVICE yuki.test.messenger/.MyMessenger 41fa2ef0 pid=6151
  Client:
    MyMessenger dump options:
      [-a] [-c] [-m] [-s]
        a[ll]: dump all info.
        c[lient]: reply target.
        m[essage]: handle message log.
        s[tate]: service state change log.
・引数指定(-p:サポートしない引数)
$ adb shell dumpsys activity service yuki.test.messenger/.MyMessenger -p
SERVICE yuki.test.messenger/.MyMessenger 41fa2ef0 pid=6151
  Client:
    Unkown argument: -p; use -h for help.
    Service state
        Service create time is Sun Jan 06 03:58:28 GMT+00:00 1980
        Current service state is BLOCKED

便利ですね。好きなタイミングで実行できるのが強力です。

注意すべき点として、、、
ログ出力の場合と同じく、dump情報で出力する情報には注意が必要です。
# Service.dumpで出力される情報はbugreportにも出力されます。
開発時以外に必要ないのであれば、ビルドタイプでdump情報を絞る等の対応を考えます。

以上です。
2013/02/14

Android:contentコマンドでContentProviderを呼び出す

Android4.1.1以降、コマンドラインユーティリティにcontentコマンドが追加されました。

サンプルコマンド:
$ content query --uri content://yuki.authority/
このコマンドは、コンテンツへのCRUD操作をコンテンツプロバイダ経由で実行します。
例えば、データベースレコードの検索や削除などが可能です。

SQLite3コマンドでも同様にデータベースの編集が可能ですが、
SQLite3コマンドの対象がデータベースであるのに対して、contentコマンドの対象はコンテンツプロバイダです。
これはつまり、
  • コンテンツプロバイダの簡単な動作確認として使用可能
  • コンテンツプロバイダを経由するため、コンテンツオブサーバへの通知も実行される
といったメリットがあります。


●contentコマンドの使い方

使い方は
& adb shell content
と入力すれば使い方が表示されます。

【query】
$ adb shell content query --uri <URI> [--projection <PROJECTION>] [--where <WHERE>] [--sort <SORT_ORDER>]

【insert】
$ adb shell content insert --uri <URI> --bind <BINDING> [--bind <BINDING>...]


【update】
$ adb shell content update --uri <URI> [--where <WHERE>]


【delete】
$ adb shell content delete --uri <URI> --bind <BINDING> [--bind <BINDING>...] [--where <WHERE>]

<URI>
    コンテンツプロバイダのURI
    (例)--uri content://hoge.authority

<PROJECTION>
    検索対象のカラム名を指定します。指定方法は <COLUMN_NAME>[:<COLUMN_NAME>...]
    (例)column1:column2:column3

<WHERE>
    SQLのWHERE句を指定します。
  (例)--where "column1='where_string'"

<SORT_OREDER>
    ソート対象の列を指定します。
   (例)--sort "column1 DESC"

<BINDING>
    次のフォーマットに従い、カラムにバインドする情報を指定します。
  <COLUMN_NAME>:<TYPE>:<COLUMN_VALUE>
  <TYPE>にはb - boolean, s - string, i - integer, l - long, f - float, d - doubleが指定可能です。
  (例)column:s:new_string


●動作確認

動作確認の対象となるテーブル情報は下記。

・Authority
yuki.authority

・テーブル情報
cid   name     type  
----  -------  --------
0     _id      INTEGER
1     number   INTEGER

・テーブル内容
_id         number
----------  ------------
1           1416078507
2           1355288181
3           380734541
4           254152153
5           18301077

【query】
# content query --uri content://yuki.authority/ --projection _id:number --where "_id BETWEEN 1 AND 3" --sort "_id DESC"
_id:number --where "_id BETWEEN 1 AND 3" --sort "_id DESC"
Row: 0 _id=3, number=380734541
Row: 1 _id=2, number=1355288181
Row: 2 _id=1, number=1416078507

【insert】
# content insert --uri content://yuki.authority/ --bind number:i:1234567
yuki.authority/ --bind number:i:1234567
-結果確認-
# content query --uri content://yuki.authority/ --projection _id:number --where "number=1234567" --sort "_id DESC"
_id:number --where "number=1234567" --sort "_id DESC"
Row: 0 _id=6, number=1234567

【update】
# content update --uri content://yuki.authority/ --bind number:i:9999
yuki.authority/ --bind number:i:9999
-結果確認-
# content query --uri content://yuki.authority/ --projection _id:number --where "number=9999"
_id:number --where "number=9999"
Row: 0 _id=1, number=9999
Row: 1 _id=2, number=9999
Row: 2 _id=3, number=9999
Row: 3 _id=4, number=9999
Row: 4 _id=5, number=9999
Row: 5 _id=6, number=9999

【delete】
# content delete --uri content://yuki.authority/ --where "_id=1"
yuki.authority/ --where "_id=1"
-結果確認-
# content query --uri content://yuki.authority/ --projection _id:number
om.example.testcprovider/ --projection _id:number
Row: 0 _id=2, number=9999
Row: 1 _id=3, number=9999
Row: 2 _id=4, number=9999
Row: 3 _id=5, number=9999
Row: 4 _id=6, number=9999

以上です。
2013/02/12

Android:DeathRecipientで他プロセスの死亡を検知する


Androidには他プロセスの死亡を検知する仕組みとしてDeathRecipientクラスが用意されています。
DeathRecipientは対象となるIBinderのホストプロセスの死亡をモニタします。

IBinder.DeathRecipient
http://developer.android.com/reference/android/os/IBinder.DeathRecipient.html

プロセス間通信時に、相手プロセスがkillされたタイミングを知ることができます。


●サンプルコード

下記はDeathRecipientを使用したサンプルコードです。
mDeathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        if (mBinder != null)
            mBinder.unlinkToDeath(this, 0);
    }
};

mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
        mBinder = binder;
        mBinder.linkToDeath(mDeathRecipient, 0);
    }
    ....
}
IBinderのホストプロセスが死亡するとbinderDied()がコールバックされます。


●ServiceConnection.onServiceDisconnectedとの違い

ServiceConnection.onServiceDisconnected()も同じような機能を持っています。
# というより、onServiceDisconnectedはDeathRecipient.binderDied()をトリガとしています。
# 参照:android.app.LoadedApk$DeathMonitor

ただし、サービスとの接続状態によっては次のような違いが出てきます。
【サービスバインド状態でのサービスホストプロセス死亡】
  • onServicedisconnectedがコールされる
  • binderDiedがコールされる

【非サービスバインド状態(サービスアンバインド後)のサービスホストプロセス死亡】
  • onServicedisconnectedはコールされない
  • binderDiedがコールされる

これは、
"サービスとの接続状態をモニタする(ServiceConnection)"
か、
"IBinderのホストプロセスの死亡をモニタする(DeathRecient)"
かの違いによるものです。


●API

・linkToDeath
対象となるBinderのホストプロセスが死亡した時の通知を受け取るRecipientを登録します。
ターゲットであるBinderが既に死亡している場合はRemoteExceptionが投げられます。

・unlinkToDeath
対象となるIBinderのホストプロセス死亡のモニタを解除します。
既に登録解除されたRecipientを引数指定するとNoSuchElementExceptionが投げられます。


●ちなみに

当然ですが、DeathRecipientは自プロセスの死亡まではlistenできません。
自プロセスがkillされると、登録したDeathRecipientインスタンスもろもろ解放されます。

2013/02/11

Android:Messengerの基本

プロセス間の双方向通信をサポートするサービスにはAIDLとMessengerが存在します。

AIDLとMessengerの主な違いは下記になります。

●AIDL
  • サービスホスト(以降ホスト)は.aidlを作成し、サービスクライアント(以降クライアント)はこれを取り込む必要がある
  • クライアントからのリクエストはBinderThread経由で、全てのリクエストが非同期通信となる
  • ホストは必要に応じてクライアントからのリクエストをスレッドセーフに扱う必要がある
  • .aidlに変更があった場合、クライアントも合わせてこれを更新する必要がある

●Messenger
  • ホストとクライアント間の通信はHandler-Messageの仕組みで実現されているため.aidlが不要
  • クライアントからのリクエストはHandler経由で通知され、全てのリクエストが同期通信となる
  • ホストとクライアント間のメッセージはMessageオブジェクトで表現される

Messengerはクライアントからのリクエストをシングルスレッドで処理するため、基本的にスレッドセーフで動作します。
また、メッセージがMessageオブジェクトで表現されるため.aidlファイルも不要です。
外部公開APIに変更があっても、.aidlファイルをクライアントに配布する必要がないため、
サービス側でMessageの規約(プロトコル)を更新するだけで済みます(互換性を意識する必要があります)

Messengerの難点として、"外部公開APIの直観性の低さ"があります。
AIDLでは外部公開APIはメソッドとして定義されます。
しかし、Messengerでは"Messageオブジェクトの組み立て方"でこれを表現する必要があります。
クライアントはMessengerと通信するためにメッセージの組み立て方を理解する必要があるのです。
(アプリローカルで使用されるHandler-Messageのそれと同じです)


●サンプルコード
Messengerを使用した双方向通信のスニペットです。

・クライアントサイド
public class MessengerClient extends Activity {
    private Messenger mServiceMessenger;
    private Messenger mSelfMessenger;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mSelfMessenger = new Messenger(new ResponseHandler());
        mConnection = new ServiceConnection() {
            ...
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                mServiceMessenger = new Messenger(service);
            }
        };
        bindService(new Intent("yuki.test.messenger.START"),
                mConnection, Service.BIND_AUTO_CREATE);

        ((Button)findViewById(R.id.button1)).setOnClickListener(
                new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mServiceMessenger != null) {
                            try {
                                Message msg = Message.obtain(null, 1);
                                msg.replyTo = mSelfMessenger;
                                mServiceMessenger.send(msg);
                            } catch (RemoteException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
    }

    private class ResponseHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Log.e("yuki", "handle response=" + msg);
        }
    }
}

・ホストサイド
public class MessengerService extends Service {
    private Messenger mServiceMessenger;

    @Override
    public void onCreate() {
        super.onCreate();
        mServiceMessenger = new Messenger(new RequestHandler());
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mServiceMessenger.getBinder();
    }

    private class RequestHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Log.e("yuki", "handle request=" + msg);

            if (msg.replyTo != null) {
                try {
                    msg.replyTo.send(Message.obtain()); // send response.
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

・クライアント⇒ホストへの通信
ホストはMessenger.getBinder()で得られるBinderをonBindコールバックで返します。
クライアントはonServiceConnectedコールバックで得られるBinderインスタンスをMessengerコンストラクタに
指定してMessengerオブジェクトを生成し、サービスとの通信路を確立します。
Messengerオブジェクトにメッセージをsendすればホストに届きます。

・ホスト⇒クライアントへの通信
クライアントはホストに渡すMessageオブジェクトのreplayToフィールドに自身のMessengerを設定します。
ホストはクライアントからのMessageオブジェクトからこれを取得し、クライアント⇒ホスト時と同様にメッセージを送信します。

・通信相手の不在
Messengerの対象となっているオブジェクト(クライアント or ホスト)が既にDeadObject化している場合はDeadObjectExceptionが投げられます。
クライアントもホストも通信相手はリモートプロセスとなるため、メッセージの送信にはRemoteExceptionのcatchが必要です。


●リクエストメッセージのキューイングによる処理遅延
Messengerはシングルスレッドモデルであるため、クライアントからのリクエストを逐次処理していきます。
※handleMessageはメインスレッド上で動作するのがデフォルトです
1つのリクエスト処理に時間が掛かると、次のリクエストを処理するのが遅れます。
これはつまり、ホストからの応答性が悪くなる可能性があるということです。
ホストとクライアントが1:多の関係にあり、リクエストの処理に手間取った場合この問題は顕著になります。

クライアントからのリクエスト処理(handleMessage)がメインスレッドで実行されることに注目します。
リクエスト処理で遅延が発生すると、onBindやonDestroy等他のメインスレッドで実行される処理にも影響が出ます。
サービスバインド要求等のリクエストも、それ以前にキューイングされたメッセージ"全て"が処理されてからでないと実行されません。

そのため、メインスレッド上でのリクエスト処理に手間取ると、
  •  bindServiceを実行しても中々サービスコネクトされない
  •  unbindServiceしたが、中々サービスがDestroyされない
といったことが起こりえます。

Messengerはシングルスレッドモデルです。
クライアントからのバインドやアンバインド要求もリクエストメッセージとして処理されるため、スレッドセーフが保証されます。
ホスト側のHandlerを別のHandlerThread(Looper)と紐付けたものをonBindで返せば、onBindやonDestroyが即実行されるようになりますが、 Messengerのメリットであるシングルスレッドモデルを崩すことになります。

・不在のクライアントからのリクエスト
クライアントからのリクエストはメッセージキューにキューイングされます。
クライアントがリクエストを送信し、ホスト側でメッセージキューからデキューされる間にクライアント側がいなくなるケースがあります。
クライアントが不在となっても、一度キューイングされたメッセージはホストに届きます。

2013/02/06

Android:エミュレータでマルチユーザ機能を確認する

JB4.2で正式にマルチユーザ機能が公開されました。
ICSから一部機能が搭載されはじめ、JB4.1では(隠された)UIが存在しましたがユーザを追加することはできませんでした。

●JB4.1でマルチユーザ機能の蓋開けを行う方法は下記。
1. 端末で利用できるユーザ数の上限を引き上げる
$ setprop fw.max_users 2
2. ユーザを作成する
$ pm create-user <userName>
3. ユーザを切り替える
[power]キー長押しの電源オプションにユーザが追加されているのでこれを選択(下図)


●JB4.2のエミュレータでマルチユーザ機能を確認する方法は下記。
JB4.2エミュレータでは、端末形状に関わらず fw.max_users値 が1で定義されています。
このため、マルチユーザ機能が蓋閉めされている状態です。
※JB4.2標準ではタブレットのみマルチユーザ機能がONになる
定数定義はframeworks/base/core/res/res/values/config.xmlのconfig_multiuserMaximumUsers

JB4.2エミュレータでマルチユーザ機能を確認するためには、ユーザ数の上限を引き上げる必要があります。
$ setprop fw.max_users 2
上記システムプロパティを編集すれば、マルチユーザ管理に必要なUIが表示されるようになります。


マルチユーザ環境下でのデータフォルダの扱い

下記はシングルユーザ時とマルチユーザ時のデータフォルダの扱いの違いです。

●シングルユーザ時
/data/data/。。。

●マルチユーザ時
ユーザを1名追加した後のデータフォルダ状態。
・ユーザ状態
Primary : 初期ユーザ
newOne  : 追加したユーザ

・Primaryユーザ用ディレクトリ
/data/data/。。。
/data/user/0/。。。

・newOneユーザ用ディレクトリ
/data/user/10/。。。

/data/userディレクトリに各ユーザ用のデータフォルダが作成される。
Primaryユーザ用のディレクトリは2つあるわけではなく、/data/user/0が/data/dataへのシンボリックリンクとして扱われます。
$ ls -la /data/user
lrwxrwxrwx root     root              2013-02-05 10:11 0 -> /data/data/
drwxrwx--x system   system            2013-02-06 06:34 10

蛇足

○設定アプリの[ユーザ]項目の表示/非表示判定箇所
com.android.settings.Settings.updateHeaderList(List<Header>)
} else if (id == R.id.user_settings) {
    if (!UserHandle.MU_ENABLED
            || !UserManager.supportsMultipleUsers()
            || Utils.isMonkeyRunning()) {
        target.remove(i);    // [ユーザ]項目を削除

android.os.UserManager.getMaxSupportedUsers()
public static int getMaxSupportedUsers() {
    // Don't allow multiple users on certain builds
    if (android.os.Build.ID.startsWith("JVP")) return 1;
    return SystemProperties.getInt("fw.max_users",
            Resources.getSystem().getInteger(
                R.integer.config_multiuserMaximumUsers));
}
# どうも"JVP"から始まるビルドIDはマルチユーザ強制非対応の様子。
2013/01/28

Android:DialogFragment.setRetainInstance(true)使用時は画面回転に注意


DialogFragmentにsetRetainInstance(true)を指定する時は注意が必要です。
下記の手順を踏むと、表示中のダイアログが閉じてしまいます。

【手順】
 1. DialogFragmentを生成し、setRetainInstance(true)を指定
 2. DialogFragmentを.show(...)メソッドで表示
 3. 画面の向きを変更する(Orientation change)

【結果】
手順2で表示したダイアログが閉じてしまいます(画像1)。
しかし、setRetainInstance(false)を設定している場合ダイアログは閉じません(画像2)。
画像1:setRetainInstance(true)

画像2:setRetainInstance(false)


setRetainInstance(true)の場合でもダイアログを閉じたくなければ、次の回避策を使います。
# コードが汚れる上に力技です。

【回避策】
onDestroyView()でsuper.onDestroyView()をコールする前に、ダイアログが持つDismissメッセージをnull化します。
@Override
public void onDestroyView() {
    if (getDialog() != null && getRetainInstance())
        getDialog().setDismissMessage(null);
    super.onDestroyView();
}
ダイアログが閉じる原因はOrientation changeによるDialogFragmentのonDestroyViewで、
Dialog.dismiss()によりメッセージキューにpushされたDISSMISSメッセージが、onCreateView後に再活性化されるためです。
この結果、ダイアログは表示された直後にdismissされてしまいます。

回避策の内容は、Orientation changeによるonDestroyViewでは、Dismissメッセージをnull化して投げさせない対応となります。

以上です。
2013/01/24

Android:manageSpaceActivityによるデータ管理




アプリ管理画面(右図)には、アプリデータを削除するためのボタン[データを消去]が用意されています。
この機能はデータベースを含むアプリデータ全てを消去するものです。
つまり、消去の対象には"アプリの設定情報"も含まれます。

ユーザはストレージの空き容量を確保する目的でこの機能を使用することがあります。
しかし、アプリデータの全削除が空き容量を確保するためのベストな手段とは限りません。
「アプリの設定情報は残して、不要で無駄にサイズの大きなデータを削除したい」と思うユーザもいるでしょう。
このような場合は、"アプリデータ管理用Activity"の採用を検討します。


●ManageSpaceActivity

開発者は[データを消去]ボタンが押された場合に、データの消去ではなく
指定のActivityを起動するようカスタマイズすることができます。

指定するにはAndroidManifest.xmlで下記のように、applicationタグの
android:manageSpaceActivity属性に、起動したいActivity名を指定します。
<application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:manageSpaceActivity=".DataManagementActivity" >
    <activity
        android:name=".DataManagementActivity"
        android:label="@string/label" >
        <intent-filter>
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
これを指定すると、[データを消去]ボタンが[容量を管理]ボタンに変更され、
ボタン押下時に指定のActivityが起動されます。


ユーザに全消去以外の選択肢を提供したい場合に使えるテクニックです。

以上です。