2012/03/29

Android:UriとUri.BuilderのAPI


android.net.UriおよびUri.BuilderクラスのAPIと、URI構文について
調査しました。

調査にあたっては下記javadocを参考としています。
http://developer.android.com/reference/android/net/Uri.html
http://developer.android.com/reference/android/net/Uri.Builder.html
http://e-class.center.yuge.ac.jp/jdk_docs/ja/api/java/net/URI.html


最高位レベルの文字列形式のURI構文は下記の通り([]内は省略可能)

 [scheme:]Scheme-Specific-Part[#fragment]


URIには「絶対URI」と「相対URI」が存在し、schemeの指定がある場合は絶対URI。
そうでない場合は相対URIとなる。

 絶対URI : yuki:android.com
 相対URI : ../../../test/hoge/


不透明URIはScheme-Specific-Part(以降「ssp」とする)部が'/'で始まらない絶対URI。

 tel:090xxxxxxxx


階層URIはssp部が'/'で始まる絶対URI。またはscheme部がない相対URI。

 http://google.com/hoge/foo
 ../../../test/hoge/

階層URIはさらに下記構文に従い解析される。

 [scheme:][//authority][path][?query][#fragment]

階層URIにおけるssp部はscheme部とfragment部の間の文字で構成される。

階層URIのAuthority部には「サーバベース」と「レジストリベース」が存在する。
「サーバベース」は下記構文に従って解析される。

  [UserInformation@]host[:port] 

「サーバベース」以外の構文は「レジストリベース」として解釈される。

階層URIのpath部が'/'で始まる場合は「絶対」、そうでない場合は「相対」と呼ばれる。


URIでは、下記9つのコンポーネントを持つ。
 scheme(String)
 ssp(String)
 authority(String)
 userInformation(String)
 host(String)
 port(Integer)
 path(String)
 query(String)
 fragment(String)


●android.net.Uri

Class Overview
イミュータブルなURI参照を提供する
URI参照はURIと'#'に続くfragment部を含む。
URIはRFC2396に準拠してURIを解析・構築する。

URIクラスはパフォーマンス向上のため引数が検証されない。
そのため、無効な入力に対する動作定義はない。
このクラスは非常に寛容で、無効な入力に対してゴミデータを返すのではなく、
指定のない限り例外がスローされる。


compareTo (Uri other)
otherとの文字列表現比較


buildUpon ()
このURI内容を引き継ぐ新たなURIビルダーを構築


encode (String s)
encode (String s, String allow)
引数sをUTF-8方式として'%'エスケープエンコードする。
未予約文字セット(英字("A-Z, "a-z")、数字("0-9")、文字("_-!.~'()*"))以外、
および引数指定されたallow以外の全ての文字をエンコードする。
Return:
 sのエンコード結果を返す。sがnullの場合はnullを返す。
Ex)
  a = a
  あ = %E3%81%82
  %E3%81%82 = %25E3%2581%2582


decode (String s)
UTF-8方式で'%'エスケープエンコードされた引数sをデコードする。
無効なオクテットはUnicodeで置き換えられる。
Return:
 sのデコード結果を返す。sがnullの場合はnullを返す。
Ex)
 a = a
 あ = あ
 %E3%81%82 = あ


parse (String uriString)
引数uriStringからURIオブジェクトを生成する。
Params:
 uriString : RFC 2396に準拠した、エンコード済みのURI文字列
Return:
 uriStringから生成したURI。
Throws:
 uriStringがnullの場合NullPointerExceptionをスローする。
Ex)
 http://あいうえお.com/#aaa = http://あいうえお.com/#aaa


fromFile (File file) 
引数fileからURIを生成する。
URIは"file://"から始まり、パスはエンコードされるが、パス区切り文字の'/'は例外とする。
Return:
 fileへのURI値を返す。
Throws:
 fileがnullの場合はNullPointerExceptionをスローする。
Ex)
 "/mnt/sdcard/test.txt = file:///mnt/sdcard/test.txt


getScheme ()
URIからscheme部を抽出する。
Return:
 相対パスの場合はnullを返す。
Ex)
 http://yuki312.blogspot.jp/ = http
 ftp://あいうえお.txt = ftp


getAuthority ()
URIからAuthority部をデコードして抽出する。
Return:
 URIのデコードされたAuthority部。存在しない場合はnullを返す。
Ex)
 http://a = a
 http://%E3%81%82 = あ
 http://%25E3%2581%2582 = %E3%81%82


getEncodedAuthority () 
URIからAuthority部を抽出する。
Return:
 URIのAuthority部。存在しない場合はnullを返す。
Ex)
 http://a = a
 http://%E3%81%82 = %E3%81%82
 http://%25E3%2581%2582 = %25E3%2581%2582


fromParts (String scheme, String ssp, String fragment) 
引数から不透明URIを生成する。
sspはエンコードされるため、このメソッドは階層的なURIを作成することができない。
Return:
 引数scheme, ssp, fragmentから構成されるURI
Throws:
 schemeかsspがnullの場合はNullPointerExceptionをスローする。
Ex)
 fromParts(a, b, c)  = a:b#c
 fromParts(あ, あ, あ)  = あ:%E3%81%82#%E3%81%82
 fromParts(%E3%81%82, %E3%81%82, %E3%81%82)  = %E3%81%82:%25E3%2581%2582#%25E3%2581%2582


getSchemeSpecificPart ()
URIからssp部をデコードして抽出する。
Return:
 デコードされたssp部。
Ex)
 http://a#fragment = //a
 http://あ#fragment = //あ
 http://%E3%81%82#fragment = //あ


getEncodedSchemeSpecificPart ()
URIからssp部を抽出する。
Return:
 ssp部。
Ex)
 http://a#fragment = //a
 http://あ#fragment = //あ
 http://%E3%81%82#fragment = //%E3%81%82


withAppendedPath (Uri baseUri, String pathSegment)
ベースURIにpathSegmentを追加した新たなURIを生成する。
Params:
 baseUri : pathSegmentを追加するURI
 pathSegment : エンコードされたパスセグメント
Return:
 ベースURIにpathSegmentを追加した新たなURIを返す。
Throws:
 baseUriがnullの場合NullPointerExceptionをスローする。
Ex)
 withAppendedPath(http://test, a)  = http://test/a
 withAppendedPath(http://test/, あ)  = http://test/あ
 withAppendedPath(http://test, %E3%81%82)  = http://test/%E3%81%82


getPath () 
URIからpath部をデコードして抽出する。
Return:
 デコードされたpath部。無効なURIや階層的URI("mailto:nobody@google.com")でない場
 合はnullを返す。
Ex)
 http:/a#fragment = /a
 http:/あ#fragment = /あ
 http:/%E3%81%82#fragment = /あ
 http://authority/a#fragment = /a


getEncodedPath ()
URIからpath部を抽出する。
Return:
 無効なURIや階層的URI("mailto:nobody@google.com")でない場合はnullを返す。
Ex)
 http://authority/a#fragment = /a
 http://authority/あ#fragment = /あ
 http://authority/%E3%81%82#fragment = /%E3%81%82


getLastPathSegment () 
URIのpath部から最後のセグメントをデコードして抽出する。
Return:
 デコードされた最後のセグメント。pathがnullの場合はnullを返す。
Ex)
 http://authority/a/b/c#fragment = c
 http://authority/あ/い/う#fragment = う
 http://authority/%E3%81%82/%E3%81%84/%E3%81%86#fragment = う


getPathSegments () 
URIからpathセグメントをデコードして抽出する。
Return:
 先頭と末尾の'/'を除いたpathセグメントを返す。
Ex)
 http://authority/a/b/c#fragment = [a, b, c]
 http://authority/あ/い/う#fragment = [あ, い, う]
 http://authority/%E3%81%82/%E3%81%84/%E3%81%86#fragment = [あ, い, う]


getQuery ()
URIからquery部をデコードして抽出する。
Return:
 デコードされたquery部。存在しない場合はnullを返す。
Ex)
 http://authority/path?q=a#fragment = q=a
 http://authority/path?q=あ#fragment = q=あ
 http://authority/path?q=%E3%81%82#fragment = q=あ
 http://authority/path?あ=あ#fragment = あ=あ
 http://authority/path?%E3%81%82=%E3%81%82#fragment = あ=あ
 http://authority/path?q=a&r=b#fragment = q=a&r=b


getEncodedQuery ()
URIからquery部を抽出する。
Return:
 エンコードされたquery部。存在しない場合はnullを返す。
Ex)
 http://authority/path?q=a#fragment = q=a
 http://authority/path?q=あ#fragment = q=あ
 http://authority/path?q=%E3%81%82#fragment = q=%E3%81%82
 http://authority/path?あ=あ#fragment = あ=あ
 http://authority/path?%E3%81%82=%E3%81%82#fragment = %E3%81%82=%E3%81%82
 http://authority/path?q=a&r=b#fragment = q=a&r=b


getQueryParameter (String key)
引数keyに対応するquery文字列を検索し、ヒットした最初の値を返す。
Params:
 key : エンコードされたkey
Return:
 keyに対応するデコードされたkey値。存在しない場合はnullを返す。
Throws:
 階層的URIで無い場合UnsupportedOperationExceptionをスローする。
 keyがnullの場合はNullPointerExceptionをスローする。
Ex)
 http://authority/path?q=a#fragment search(q) = a
 http://authority/path?q=あ#fragment search(q) = あ
 http://authority/path?q=%E3%81%82#fragment search(q) = あ
 http://authority/path?%E3%81%82=true#fragment search(あ) = null
 http://authority/path?あ=true#fragment search(%E3%81%82) = true
 http://authority/path?q=a&r=b#fragment search(r) = b
 http://authority/path?q=a&r=b#fragment search(s) = null


getBooleanQueryParameter (String key, boolean defaultValue)
引数keyに対応するquery文字列を検索し、ヒットした最初の値を返す。
値はbooleanとして解釈される。
値が"false"と"0"はfalse, それ以外はtrueとして解釈される。
Params:
 key : エンコードされたkey
 defaultValue : keyに対応するクエリがない場合のデフォルト値
Return:
 クエリパラメータkeyに対応するブール値解釈された値
Ex)
 http://authority/path?a=true#fragment search(a) defValue(false) = true
 http://authority/path?あ=true#fragment search(あ) defValue(false) = false
 http://authority/path?%E3%81%82=true#fragment search(あ) defValue(false) = true
 http://authority/path?あ=true#fragment search(%E3%81%82) defValue(false) = false
 http://authority/path?a=hoge#fragment search(a) defValue(false) = true
 http://authority/path?q=true#fragment search(r) defValue(false) = false


getQueryParameters (String key)
引数keyに対応するクエリを検索し、ヒットした値をデコードして返す。
Params:
 key : エンコードされたkey
Return:
 デコードされた値セット
Throws:
 階層的URIでない場合はUnsupportedOperationExceptionをスローする。
 keyがnullの場合はNullPointerExceptionをスローする。
Ex)
 http://authority/path?q=a#fragment search(q) = [a]
 http://authority/path?q=あ#fragment search(q) = [あ]
 http://authority/path?q=%E3%81%82#fragment search(q) = [あ]
 http://authority/path?q=a&q=b#fragment search(q) = [a, b]


getQueryParameterNames ()
クエリパラメータの一意の名前セットをデコードして取得する。
名前セットは出現順となる。
Return:
 デコードされたクエリパラメータ名のセット。
Throws:
 階層URIでない場合UnsupportedOperationExceptionをスローする。
Ex)
 http://authority/path?a=v#fragment search(q) = [a]
 http://authority/path?あ=v#fragment search(q) = [あ]
 http://authority/path?%E3%81%82=v#fragment search(q) = [あ]
 http://authority/path?q=a&r=b#fragment search(q) = [q, r]


getFragment ()
URIからfragment部をデコードして抽出する。
fragment部は'#'以降の全てとなる。
Return:
 URIのデコードされたfragment部。存在しない場合はnullを返す。
Ex)
 http://authority/path#a = a
 http://authority/path#あ = あ
 http://authority/path#%E3%81%82 = あ


getEncodedFragment () 
URIからFragment部を抽出する。
Return:
 '#'以降の全ての値を返す。
Ex)
 http://authority/path#a = a
 http://authority/path#あ = あ
 http://authority/path#%E3%81%82 = %E3%81%82


getUserInfo ()
URIのAuthority部からUserInformation部をデコードして抽出する。
Return:
 URIのUserInformation部。存在しない場合はnullを返す。
Ex)
 http://a@host.com:8080/path = a
 http://あ@host.com:8080/path = あ
 http://%E3%81%82@host.com:8080/path = あ


getEncodedUserInfo ()
URIのAuthority部からUserInformation部を抽出する。
Return:
 URIのUserInformation部。存在しない場合はnullを返す。
Ex)
 http://a@host.com:8080/path = a
 http://あ@host.com:8080/path = あ
 http://%E3%81%82@host.com:8080/path = %E3%81%82


getHost ()
URIのAuthority部からhost部をデコードして抽出する。
Return:
 URIのhost部。存在しない場合はnullを返す。
Ex)
 http://userInfo@a.com:8080/path = a.com
 http://userInfo@あ.com:8080/path = あ.com
 http://userInfo@%E3%81%82.com:8080/path = あ.com


getPort ()
URIのAuthority部からport部を抽出する。
Return:
 URIのport部。無効、あるいは存在しない場合は-1を返す。
Ex)
 http://userInfo@a.com:8080/path = 8080


isAbsolute ()
絶対URIであるかを判定する。
明示的にschemeが含まれている場合などに該当する。
Return:
 絶対URIである場合true. 相対URIの場合等はfalseを返す。
Ex)
 http://hoge/foo/bar = true
 /hoge/foo/bar = false
 ../hoge/foo/bar = false


isRelative ()
相対的URIであるかを判定する。
schemeが含まれていない場合などが該当する。
Return:
 相対URIの場合はtrue。絶対URIの場合等はfalseを返す。
Ex)
 http://hoge/foo/bar = false
 /hoge/foo/bar = true
 ../hoge/foo/bar = true


isHierarchical ()
"http://google.com"のような階層的URIであるかを判定する。
ssp部が'/'で始まる場合、絶対URIは階層的URIとなる。
相対URIは常に階層的URIとなる。
Ex)
 http://hoge/foo/bar = true
 /hoge/foo/bar = true
 tel:090xxxxxxxx = false


isOpaque ()
"mailto:nobody@google.com"のような不透明URIであるかを判定する。
ssp部が'/'で始まらない場合は不透明URIとなる。
Ex)
 http://hoge/foo/bar = false
 /hoge/foo/bar = false
 tel:090xxxxxxxx = true


=================================================

●android.net.Uri.Builder

Class Overview
URI参照を構築・操作するためのヘルパークラス。
このクラスはスレッドセーフではない。

階層的絶対URIは"://?#"パターンに従う。

相対URI(常に階層構造である)は"?#"、"//?# "パターンのいずれかに従う。

不透明URIは":#"パターンに従う。

既存のURIからなるビルダーを取得するにはUri.buildUpon()を使用する。


build ()
ビルダに設定した属性をもつURIを構築する。
Throws:
 不透明URIかつscheme部がnullの場合URIUnsupportedOperationExceptionをスローする。


scheme (String scheme) 
scheme部を設定する。
Params:
 scheme : scheme名、または相対URIの場合null
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = a://authority/path?queryKey=value#fragment
 あ = あ://authority/path?queryKey=value#fragment
 %E3%81%82 = %E3%81%82://authority/path?queryKey=value#fragment


opaquePart (String opaquePart) 
引数opaquePartをエンコードして不透明ssp部として設定する。
Params:
 opaquePart : デコードされた不透明ssp部
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme:a#fragment
 あ = scheme:%E3%81%82#fragment
 %E3%81%82 = scheme:%25E3%2581%2582#fragment


encodedOpaquePart (String opaquePart)
引数opaquePartを不透明ssp部に設定する。
Params:
 opaquePart : エンコードされた不透明ssp部
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme:a#fragment
 あ = scheme:あ#fragment
 %E3%81%82 = scheme:%E3%81%82#fragment


authority (String authority)
引数authorityをAuthority部にエンコードして設定する。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme://a/path?queryKey=value#fragment
 あ = scheme://%E3%81%82/path?queryKey=value#fragment
 %E3%81%82 = scheme://%25E3%2581%2582/path?queryKey=value#fragment


encodedAuthority (String authority)
引数authorityをAuthority部に設定する。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme://a/path?queryKey=value#fragment
 あ = scheme://あ/path?queryKey=value#fragment
 %E3%81%82 = scheme://%E3%81%82/path?queryKey=value#fragment


path (String path) 
引数pathをpath部にエンコードして設定する。
文字'/'はそのままエンコードされない。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme://authority/a?queryKey=value#fragment
 あ = scheme://authority/%E3%81%82?queryKey=value#fragment
 %E3%81%82 = scheme://authority/%25E3%2581%2582?queryKey=value#fragment
 a/b = scheme://authority/a/b?queryKey=value#fragment


encodedPath (String path)
引数pathをpath部に設定する。
パスが非null、かつ'/'で開始していない、かつAuthority or Scheme指定している場合、
ビルダはpathに'/'を付加する。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme://authority/a?queryKey=value#fragment
 あ = scheme://authority/あ?queryKey=value#fragment
 %E3%81%82 = scheme://authority/%E3%81%82?queryKey=value#fragment
 a/b = scheme://authority/a/b?queryKey=value#fragment


appendPath (String newSegment)
引数newSegmentをpath部にエンコードして追加する。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme://authority/path/a?queryKey=value#fragment
 あ = scheme://authority/path/%E3%81%82?queryKey=value#fragment
 %E3%81%82 = scheme://authority/path/%25E3%2581%2582?queryKey=value#fragment
 a/b = scheme://authority/path/a%2Fb?queryKey=value#fragment


appendEncodedPath (String newSegment)
引数newSegmentをpath部に追加する。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme://authority/path/a?queryKey=value#fragment
 あ = scheme://authority/path/あ?queryKey=value#fragment
 %E3%81%82 = scheme://authority/path/%E3%81%82?queryKey=value#fragment
 a/b = scheme://authority/path/a/b?queryKey=value#fragment



query (String query)
引数queryをquery部にエンコードして設定する。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme://authority/path?a#fragment
 a=a = scheme://authority/path?a%3Da#fragment
 あ=あ = scheme://authority/path?%E3%81%82%3D%E3%81%82#fragment
 %E3%81%82=%E3%81%82 = scheme://authority/path?%25E3%2581%2582%3D%25E3%2581%2582#fragment


encodedQuery (String query)
引数queryをquery部に設定する。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a=a = scheme://authority/path?a=a#fragment
 あ=あ = scheme://authority/path?あ=あ#fragment
 %E3%81%82=%E3%81%82 = scheme://authority/path?%E3%81%82=%E3%81%82#fragment
 scheme://authority/path#fragment


appendQueryParameter (String key, String value)
引数keyとvalueをquery部にエンコードして追加する。
Params:
 key : エンコードされるキー
 value : エンコードされる値
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme://authority/path?queryKey=value&a=a#fragment
 あ = scheme://authority/path?queryKey=value&%E3%81%82=%E3%81%82#fragment
 %E3%81%82 = scheme://authority/path?queryKey=value&%25E3%2581%2582=%25E3%2581%2582#fragment


clearQuery ()
設定したクエリを消去します。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、


fragment (String fragment) 
引数fragmentをframgnet部に設定する。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、


encodedFragment (String fragment)
引数fragmentをfragment部にエンコードして設定する。
Ex)
 "scheme://authority/path?queryKey=value#fragment"を基本形として、、、
 a = scheme://authority/path?queryKey=value#a
 あ = scheme://authority/path?queryKey=value#%E3%81%82
 %E3%81%82 = scheme://authority/path?queryKey=value#%25E3%2581%2582

以上です。

2012/03/26

Android:ランチャーActivityを起動しているのは誰?


ActivityStack.resumeTopActivityLocked内で、Activityが1つも起動されていない場合、
ランチャーを起動するようになっています。

これにより、端末起動時にはまずランチャーが起動されることになります。

・ランチャー起動までのスタック
com.android.server.am.ActivityManagerService.startHomeActivityLocked()
com.android.server.am.ActivityStack.resumeTopActivityLocked(ActivityRecord)
com.android.server.am.ActivityManagerService.systemReady(Runnable)

ActivityManagerService.java
public void systemReady(final Runnable goingCallback) {
    mMainStack.resumeTopActivityLocked(null);

ActivityStack.java
final boolean resumeTopActivityLocked(ActivityRecord prev) {
    // Find the first activity that is not finishing.
    ActivityRecord next = topRunningActivityLocked(null);

    if (next == null) {
        // There are no more activities!  Let's just start up the
        // Launcher...
        if (mMainStack) {
            return mService.startHomeActivityLocked();  // ランチャー起動
        }
    }

systemReadyが呼ばれる契機は、Androidの起動シーケンスに関わる話題なので割愛。

以上です。

Android:CursorAdapterコンストラクタの一部非推奨化

Android3.0以降、CursorAdapterコンストラクタの引数"flag"に制約が追加されました。
この変更はサブクラスであるResourceCursorAdapterやSimpleCursorAdapterにも影響します。

Android2.3.x以前は、コンストラクタの引数"flag"に設定可能な値としてFLAG_AUTO_REQUERY
が存在していましたが、Android3.0以降これは非推奨となりました。
http://developer.android.com/reference/android/widget/CursorAdapter.html#FLAG_AUTO_REQUERY

●非推奨となるAPI
注意すべきポイントは、flag指定を省略したコンストラクタである下記は、デフォルトで
flagにFLAG_AUTO_REQUERYを設定します。
CursorAdapter(Context context, Cursor c)

また、ResourceCursorAdapterやSimpleCursorAdapterが持つ下記コンストラクタも、
内部でCursorAdapter(Context context, Cursor c)を呼んでいるため非推奨となりました。
  • ResourceCursorAdapter(Context context, int layout, Cursor c)
  • SimpleCursorAdapter (Context context, int layout, Cursor c, String[] from, int[] to)

また、下記コンストラクタのautoRequeryにtureを設定するのもNGです。
CursorAdapter(Context context, Cursor c, boolean autoRequery)


●非推奨となった経緯
FLAG_AUTO_REQUERYは監視コンテンツの変更を検知した際に自動でrequeryさせるための
フラグです。
しかし、requeryによるクエリ実行処理はUIスレッド上で行われるため、パフォーマンス
上の観点から非推奨となりました。
# Android3.0でLoaderが追加されたように、UIスレッドでのクエリ実行は基本NGです。


●代替案
flagにはFLAG_REGISTER_CONTENT_OBSERVERをセットするか0を設定することです。
これらの値で初期化されたCursorAdapterはrequeryが実行されません。
また、autoRequeryは必ずfalseを設定するようにします。

クエリの再発行が必要である場合はCursorLoaderの使用を考えることができます。
Loaderによるクエリ実行はUIスレッドでは行われません。
下記のようにすれば、LoaderによるCursorの取得を契機にAdapterのCursorを更新するこ
とが可能です。

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor result) {
    mAdapter.swapCursor(result);
}
※LoaderManagerコールバック内で、結果セットを破棄(cursor.close)してはいけません。
CursorAdapter.changeCursor(Cursor)はcursorをcloseするため、ここではswapCursor(Cursor)
を使用する必要があります。

以上です。

Android:ResourceCursorAdapterを使ってリストを描画する


ResourceCursorAdapterは、レイアウトXMLで定義されたViewをリスト項目として生成する
簡易なAdapterです。
DBの値をカスタマイズされたレイアウトでリスト表示したい場合に便利です。

ResourceCursorAdapterを利用する上でのポイントは下記2つのAPIです。
newView(Context, Cursor, ViewGroup)
bindView(View, Context, Cursor)

通例では、それぞれのAPIで下記の処理を実装します。
newView
  • 表示するViewをsuper.newViewで生成・取得
  • ViewHolderにView参照をキャッシュ

bindView
  • ViewHolder経由で、表示する情報を設定

newViewはViewの生成、bindViewはViewへの情報割当を行います。
下記は、list_itemで指定されたレイアウトをリスト表示する簡単なサンプルです。
class ViewHolder {
    private TextView label;
}

class CallLogAdapter extends ResourceCursorAdapter {
    public CallLogAdapter() {
        super(MyListFragment.this.getActivity(),
                R.layout.list_item, null, 0);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        View v = super.newView(context, cursor, parent);

        ViewHolder holder = new ViewHolder();
        holder.label = (TextView)v.findViewById(R.id.label);
        v.setTag(holder);

        return v;
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        ViewHolder holder = (ViewHolder)view.getTag();
        holder.label.setText(cursor.getString(3/*number*/));
    }
};

layout/list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/label"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />

</LinearLayout>

以上です。

2012/03/25

Android:ActionBarに一瞬表示されるタイトルを非表示にする方法

アクションバーにタブを表示し、ホームとタイトルを非表示にした下記のような画面を
作成する場合、

特に何も指定しないと、アプリ起動時に一瞬タイトルが表示されてしまいます。

これを防ぐには、ホーム(アプリアイコン)とタイトルの非表示をStyleリソースで指定す
るととうまくいきます。

<resources>
    <style name="TopPageTheme" parent="@android:style/Theme.Holo.Light">
        <item name="android:actionBarStyle">@style/TopPageStyle</item>
    </style>

    <style name="TopPageStyle" parent="@android:style/Widget.Holo.Light.ActionBar">
        <item name="android:navigationMode">tabMode</item>
        <item name="android:displayOptions"></item>
    </style>
</resources>

詳細を下記に記載します。

Fragmentが導入されて以降、TabHostが廃止となりました。
そのため、Android3.0以降でタブを実現したい場合はActionBarのナビゲーションモード
にタブモードを設定する必要があります。

・ナビゲーションモードにタブモードを設定するコード
ActionBar actionBar = getActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

ナビゲーションモードにタブモードを設定すると下記のような画面構成になりますが、

ハンドセットでタイトルとタブが並ぶのは不恰好に見え、画面領域が勿体ないので、
タイトル部分を非表示にすることがよくあります。

タイトル部分を非表示にしたい場合はホームとタイトルを非表示に設定します。
・ホームとタイトルを非表示するコード
ActionBar actionBar = getActionBar();
actionBar.setDisplayShowHomeEnabled(false);
actionBar.setDisplayShowTitleEnabled(false);

これを実行すると、期待通りタイトルが非表示でタブだけが表示される画面になりました。


しかし、このアプリを起動すると一瞬、非表示にしたはずのタイトル部分が表示されます。

一瞬とはいえ、これでは少々不恰好です。
タイトルを一瞬でも表示されないようにするには、ホームとタイトルの非表示指定を
Styleリソースに定義し、Activityのテーマとしてこれを読み込みます。

Styles.xml
<resources>
    <style name="TopPageTheme" parent="android:Theme.Holo.Light">
        <item name="android:actionBarStyle">@style/TopPageStyle</item>
    </style>

    <style name="TopPageStyle" parent="android:Widget.Holo.Light.ActionBar">
        <item name="android:navigationMode">tabMode</item>
        <item name="android:displayOptions"></item>
    </style>
</resources>

AndroidManifest.xml
<activity
        android:theme="@style/TopPageTheme"
        ... >

こうすることで、起動時にタイトルが表示されることはなくなります。
しかし、アクションバーの下線部はまだ一瞬表示されてしまいます。

アクションバー下線部はAndroid標準のアプリでも同様に表示されるため、非表示にする
のが困難な可能性があります。
非表示とする方法については、わかり次第追記します。

以上です。

2012/03/22

Android:CountDownLatchで同期をとる


Androidに限った話ではありませんが、java.util.concurrentパッケージは同期・非同期
処理を実装する上で便利なクラスが数多くあります。

今回はそんなconcurrentパッケージから、他スレッドでの操作完了を待機する同期支援
クラスのCountDownLatchを使ってみます。
http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/java/util/concurrent/CountDownLatch.html

CountDownLatchクラスの主なメソッドはawaitとcountDownです。
CountDownLatchは単純なON/OFFを持ったラッチとして機能します。

awaitを呼び出したスレッドは、countDownを呼び出すスレッドによりラッチが開放される
まで待機します。
ラッチの開放にかかるアクション回数はコンストラクタで指定します。
アクションをN回で初期化された場合、countDownがN回実行されるまでawaitを呼び出した
スレッドは待機します。

例えば、2つのサービスをbindServiceで接続する必要があり、
とある処理αを実行する準備として、2つのサービスに接続済みである必要がある場合。
CountDownLatchを使用してサービスへの接続を待機する簡単なコードは下記になります。
// バインドするサービスクラス群
private final Class<?>[] SERVICE_CLASSES = new Class<?>[] {
        MyServiceA.class,
        MyServiceB.class
};

// バインドするサービス数でN初期化
private final CountDownLatch mLatch = new CountDownLatch(SERVICE_CLASSES.length);

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Log.e("yuki", "onServiceConnected");

        // サービスが接続されたらカウントダウン
        mLatch.countDown();
    }

    //...
};

private void hoge() {
    // サービスの接続待ちをするスレッド
    new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... params) {
            try {
                // 全サービスの接続待ち
                mLatch.await();
            } catch(InterruptedException e) {
            }
            return null;
        }

        protected void onPostExecute(Void result) {
            Log.e("yuki", "全サービスの接続完了");
        };
    }.execute();

    // サービスをバインド
    for (Class<?> serviceClass : SERVICE_CLASSES) {
        Intent intent = new Intent(this, serviceClass);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }
}

# 2012/06/15追記

IBM DeveloperWorksにあるCountDownLatchサンプルコード
「CountDownLatch: レースに行きましょう」が素敵です。
http://www.ibm.com/developerworks/jp/java/library/j-5things5.html#N100B3

以上です。

2012/03/19

Android:BOOT_COMPLETEDブロードキャストが投げられた後か判定する方法

ACTION_BOOT_COMPLETEDブロードキャストインテントが投げられたかどうかを判定したい
場合は、システムプロパティ"sys.boot_completed"を参照します。

設定元はActivityManagerServiceクラスのfinishBootingメソッド
// Tell anyone interested that we are done booting!
SystemProperties.set("sys.boot_completed", "1");
broadcastIntentLocked(null, null,
        new Intent(Intent.ACTION_BOOT_COMPLETED, null),
        null, null, 0, null, null,
        android.Manifest.permission.RECEIVE_BOOT_COMPLETED,
        false, false, MY_PID, Process.SYSTEM_UID);

BootCompletedが投げられる直前にシステムプロパティ"sys.boot_completed"が"1"に設定
されます。

以上です。
2012/03/17

Android:Contextを生成しているモジュールは?

getContentResolverの戻り値を決定している箇所を特定するために、Contextの実体につ
いて調査しました。

getContentResolverはContextクラスで定義された抽象メソッドです。
public abstract ContentResolver getContentResolver();

これを実装した具象クラスを探ります。

getContentResolverを呼び出すActivityクラスの型階層は下記です。



Activityとその親クラスであるContextThemeWrapperはgetContentResolverを実装していません。
さらに親クラスのContextWrapperを見ると、その実装があります。

@Override
public ContentResolver getContentResolver() {
    return mBase.getContentResolver();
}

mBaseのgetContentResolverを呼び出しています(mBaseはContext型)。
続いてmBaseの実体を追います。

mBaseはContextWrapper.attachBaseContextでアタッチされています。
protected void attachBaseContext(Context base) {
    if (mBase != null) {
        throw new IllegalStateException("Base context already set");
    }
    mBase = base;
}

アタッチされるまでのスタックトレースは下記になります。
ContextWrapper.attachBaseContext(Context)
ContextThemeWrapper.attachBaseContext(Context)
Activity.attach(Context, ActivityThread, Instrumentation, , ,...)
ActivityThread.performLaunchActivity(ActivityThread$ActivityClientRecord, Intent)

attachBaseContextに渡されるContextオブジェクトのbaseは、ActivityThreadクラスの
performLaunchActivityメソッドで作られるContextImplオブジェクトであることがわかります。
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// ...
    ContextImpl appContext = new ContextImpl();
    appContext.init(r.packageInfo, r.token, this);
    appContext.setOuterContext(activity);

    activity.attach(appContext, this, getInstrumentation(), r.token,
        r.ident, app, r.intent, r.activityInfo, title, r.parent,
        r.embeddedID, r.lastNonConfigurationInstances, config);
// ...
}

ContextImplを見ると、getContentResolverの実装があります。
アプリでよく使うgetContentResolverではこのメソッドが最終的に呼ばれます。
@Override
public ContentResolver getContentResolver() {
    return mContentResolver;
}

ContextImplクラスを含めた型階層は下記です。


ついでに、mContentResolverの実体はContextImplの内部クラスとして定義されている
ApplicationContentResolverです。
private static final class ApplicationContentResolver extends ContentResolver {
    public ApplicationContentResolver(Context context, ActivityThread mainThread) {
        super(context);
        mMainThread = mainThread;
    }

    @Override
    protected IContentProvider acquireProvider(Context context, String name) {
        return mMainThread.acquireProvider(context, name);
    }

    @Override
    protected IContentProvider acquireExistingProvider(Context context, String name) {
        return mMainThread.acquireExistingProvider(context, name);
    }

    @Override
    public boolean releaseProvider(IContentProvider provider) {
        return mMainThread.releaseProvider(provider);
    }

    private final ActivityThread mMainThread;
}

以上です。
2012/03/16

Android:緊急呼発信時のシーケンス

前回に続き、今回は緊急呼発信時のシーケンスです。

通常時の緊急呼発信シーケンスでもよいのですが、本稿では機内モード設定時の緊急呼発
信シーケンスを見ていきます。

処理を追う前に、下記の前提知識です。
  • 緊急呼発信は3rdパーティアプリから利用することが出来ない
  • 緊急呼は機内モード設定時でも発信を試みる
緊急呼は命の危険に関わる為、扱いは慎重に行う必要があります。

前者の仕様に関して、通常呼では3rdパーティアプリが音声発信シーケンスの一部に関与
することができましたが、緊急呼ではそれができません。
ただし、ACTION_NEW_OUTGOING_CALLのブロードキャストは緊急呼発信時も送信されるので、
これを検知することは可能ですが、緊急呼発信処理の妨げとならないよう細心の注意が
必要です。

後者の仕様に関して、緊急呼は機内モードを解除して発信しようとします。
# 下記は解除→発信時の画面



緊急呼のテストはエミュレータや擬似網環境で行う必要があります。
実網を使って緊急呼をテストしてはいけません。
今回はエミュレータで緊急呼テストしています。
デフォルトでエミュレータの緊急通報番号は911です。
好きな番号を緊急通報番号として判定させたい場合はisEmergencyNumberがtrueを返す
ように下記のコマンドを実行します。

# 110番を緊急呼番号として登録
adb shell setprop ril.ecclist 110

緊急呼発信時のシーケンスも通常呼発信時とほぼ同じです。


●緊急呼発信の開始
通常呼発信と同じくaction.CALLのIntentをOutgoingCallBroadcasterが受信します。
ただし、ここで緊急通報番号と判断されれば緊急呼発信シーケンスに移ります。

・com.android.phone.OutgoingCallBroadcaster.java
protected void onCreate(Bundle icicle) {
    // If true, this flag will indicate that the current call is a special kind
    // of call (most likely an emergency number) that 3rd parties aren't allowed
    // to intercept or affect in any way.  (In that case, we start the call
    // immediately rather than going through the NEW_OUTGOING_CALL sequence.)
    boolean callNow;

    } else if (Intent.ACTION_CALL_EMERGENCY.equals(action)) {
        callNow = true;
    }

    if (callNow) {
        // This is a special kind of call (most likely an emergency number)
        // that 3rd parties aren't allowed to intercept or affect in any way.
        // So initiate the outgoing call immediately.

        PhoneApp.getInstance().callController.placeCall(intent);

        // Note we do *not* "return" here, but instead continue and
        // send the ACTION_NEW_OUTGOING_CALL broadcast like for any
        // other outgoing call.  (But when the broadcast finally
        // reaches the OutgoingCallReceiver, we'll know not to
        // initiate the call again because of the presence of the
        // EXTRA_ALREADY_CALLED extra.)
    }

    PhoneUtils.checkAndCopyPhoneProviderExtras(intent, broadcastIntent);
    broadcastIntent.putExtra(EXTRA_ALREADY_CALLED, callNow);
    sendOrderedBroadcast(broadcastIntent, PERMISSION, new OutgoingCallReceiver(),
                         null,  // scheduler
                         Activity.RESULT_OK,  // initialCode
                         number,  // initialData: initial value for the result data
                         null);  // initialExtras
}

緊急通報番号と判断されるとcallNowフラグがtrueになり、即placeCallが呼ばれ発信動作
に移ります。
コメントにもあるように、緊急通報番号と判定されても即リターンはされず、
通常呼発信シーケンスと似たようなプロセスを辿らせてACTION_NEW_OUTGOING_CALL
ブロードキャストIntentが投げられます。

通常呼シーケンスではACTION_NEW_OUTGOING_CALLをOutgoingCallReceiverが受信した後、
音声発信動作に移りますが、緊急呼発信シーケンスの場合、OutgoingCallReceiverが受信
する時点では既に緊急呼が発信されている為、発信処理をキャンセルする必要があります。

そのため、callNow変数をEXTRA_ALREADY_CALLEDに格納してOutgoingCallReceiver側でこれ
を参照し処理を中断します。

・com.android.phone.OutgoingCallBroadcaster$OutgoingCallReceiver
public void doReceive(Context context, Intent intent) {
    boolean alreadyCalled;
    String number;
    String originalUri;

    alreadyCalled = intent.getBooleanExtra(
            OutgoingCallBroadcaster.EXTRA_ALREADY_CALLED, false);
    if (alreadyCalled) {
        if (DBG) Log.v(TAG, "CALL already placed -- returning.");
        return;
    }
}

3rdパーティアプリが通常呼発信時に電話番号(resultData)を編集するかもしれませんが、
緊急呼シーケンスではresultDataの内容は無視されます

緊急呼発信シーケンスに戻り、placeCallの続きを追います。

・com.android.phone.CallController.java
public void placeCall(Intent intent) {
    CallStatusCode status = placeCallInternal(intent);
    mApp.displayCallScreen();
}

private CallStatusCode placeCallInternal(Intent intent) {
    // update okToCallStatus based on new phone
    okToCallStatus = checkIfOkToInitiateOutgoingCall(
            phone.getServiceState().getState());
    if (okToCallStatus != CallStatusCode.SUCCESS) {
        // If this is an emergency call, launch the EmergencyCallHelperService
        // to turn on the radio and retry the call.
        if (isEmergencyNumber && (okToCallStatus == CallStatusCode.POWER_OFF)) {
            Log.i(TAG, "placeCall: Trying to make emergency call while POWER_OFF!");

            // If needed, lazily instantiate an EmergencyCallHelper instance.
            synchronized (this) {
                if (mEmergencyCallHelper == null) {
                    mEmergencyCallHelper = new EmergencyCallHelper(this);
                }
            }

            // ...and kick off the "emergency call from airplane mode" sequence.
            mEmergencyCallHelper.startEmergencyCallFromAirplaneModeSequence(number);

            // Finally, return CallStatusCode.SUCCESS right now so
            // that the in-call UI will remain visible (in order to
            // display the progress indication.)
            // TODO: or maybe it would be more clear to return a whole
            // new CallStatusCode called "TURNING_ON_RADIO" here.
            // That way, we'd update inCallUiState.progressIndication from
            // the handleOutgoingCallError() method, rather than here.
            return CallStatusCode.SUCCESS;
        }
    }
}

もし、機内モード等により、音声発信の準備が整っていないとokToCallStatusはSUCCESS
とならず、機内モードを解除して発信を成功させようとするシーケンスに入ります。
このシーケンスではEmergencyCallHelperがメインとなります。

・com.android.phone.EmergencyCallHelper.java
public class EmergencyCallHelper extends Handler {
    public void startEmergencyCallFromAirplaneModeSequence(String number) {
        if (DBG) log("startEmergencyCallFromAirplaneModeSequence('" + number + "')...");
        Message msg = obtainMessage(START_SEQUENCE, number);
        sendMessage(msg);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case START_SEQUENCE:
                startSequenceInternal(msg);
                break;
            case SERVICE_STATE_CHANGED:
                onServiceStateChanged(msg);
                break;
            case RETRY_TIMEOUT:
                onRetryTimeout();
                break;
        }
    }

    private void startSequenceInternal(Message msg) {
        // No need to check the current service state here, since the only
        // reason the CallController would call this method in the first
        // place is if the radio is powered-off.
        //
        // So just go ahead and turn the radio on.

        powerOnRadio();  // We'll get an onServiceStateChanged() callback
                         // when the radio successfully comes up.

        // Next step: when the SERVICE_STATE_CHANGED event comes in,
        // we'll retry the call; see placeEmergencyCall();
        // But also, just in case, start a timer to make sure we'll retry
        // the call even if the SERVICE_STATE_CHANGED event never comes in
        // for some reason.
        startRetryTimer();
    }
}

EmergencyCallHelperはHandlerを継承したクラスです。
startSequenceInternalメソッドのpowerOnRadioで機内モードの解除を試みます。
ただし、機内モードが解除できなくてもリトライ処理がstartRetryTimerで準備されます。

・com.android.phone.EmergencyCallHelper.java
private void powerOnRadio() {
    if (DBG) log("- powerOnRadio()...");

    // We're about to turn on the radio, so arrange to be notified
    // when the sequence is complete.
    registerForServiceStateChanged();

    // If airplane mode is on, we turn it off the same way that the
    // Settings activity turns it off.
    if (Settings.System.getInt(mApp.getContentResolver(),
                               Settings.System.AIRPLANE_MODE_ON, 0) > 0) {
        Settings.System.putInt(mApp.getContentResolver(),
                               Settings.System.AIRPLANE_MODE_ON, 0);

        // Post the intent
        Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
        intent.putExtra("state", false);
        mApp.sendBroadcast(intent);
    } else {
        // Otherwise, for some strange reason the radio is off
        // (even though the Settings database doesn't think we're
        // in airplane mode.)  In this case just turn the radio
        // back on.
        if (DBG) log("==> (Apparently) not in airplane mode; manually powering radio on...");
        mPhone.setRadioPower(true);
    }
}

private void startRetryTimer() {
    removeMessages(RETRY_TIMEOUT);
    sendEmptyMessageDelayed(RETRY_TIMEOUT, TIME_BETWEEN_RETRIES);
}

powerOnRadioでは、電波の回復を検知するためにregisterForServiceStateChangedリスナー
が登録されます。

・com.android.phone.EmergencyCallHelper.java
private void registerForServiceStateChanged() {
    // Unregister first, just to make sure we never register ourselves
    // twice.  (We need this because Phone.registerForServiceStateChanged()
    // does not prevent multiple registration of the same handler.)
    mPhone.unregisterForServiceStateChanged(this);  // Safe even if not currently registered
    mPhone.registerForServiceStateChanged(this, SERVICE_STATE_CHANGED, null);
}

電波状態が変化すればSERVICE_STATE_CHANGEDが投げられます。
handleMessageでこれを受け取るとonServiceStateChangedをコールします。
onServiceStateChangedでは電波状態を再確認し、発信可能と判断できれば緊急呼発信を
行います。発信可能と判断されなければ何もしません。

・com.android.phone.EmergencyCallHelper.java
private void onServiceStateChanged(Message msg) {
    ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result;

    // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY,
    // it's finally OK to place the emergency call.
    boolean okToCall = (state.getState() == ServiceState.STATE_IN_SERVICE)
            || (state.getState() == ServiceState.STATE_EMERGENCY_ONLY);

    if (okToCall) {
        // Woo hoo!  It's OK to actually place the call.

        // Deregister for the service state change events.
        unregisterForServiceStateChanged();
        placeEmergencyCall();
    }
}

発信可能と判断されなかった場合は、startRetryTimerで投げたRETRY_TIMEOUTメッセージが
ハンドルされます。
RETRY_TIMEOUTは5秒のディレイが掛けられています。
RETRY_TIMEOUTメッセージを5秒後に受け取るとonRetryTimeoutメソッドをコールします。

・com.android.phone.EmergencyCallHelper.java
private void onRetryTimeout() {
    Phone.State phoneState = mCM.getState();
    int serviceState = mPhone.getServiceState().getState();

    if (serviceState != ServiceState.STATE_POWER_OFF) {
        // Woo hoo -- we successfully got out of airplane mode.
        placeEmergencyCall();  // If the call fails, placeEmergencyCall()
                               // will schedule a retry.
    } else {
        powerOnRadio();  // Again, we'll (hopefully) get an onServiceStateChanged()
                         // callback when the radio successfully comes up.

        // ...and also set a fresh retry timer (or just bail out
        // totally if we've had too many failures.)
        scheduleRetryOrBailOut();
    }
}

onRetryTimeoutでは、リトライ処理を開始する前に電波状態を確認し、有効な場合は緊急
呼発信を試みます。
ただし、電波状態が有効でない場合はscheduleRetryOrBailOutでリトライ処理を開始します。

・com.android.phone.EmergencyCallHelper.java
private void scheduleRetryOrBailOut() {
    mNumRetriesSoFar++;

    if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
        // ...and have the InCallScreen display a generic failure
        // message.
        mApp.inCallUiState.setPendingCallStatusCode(CallStatusCode.CALL_FAILED);
    } else {
        startRetryTimer();
        mApp.inCallUiState.setProgressIndication(ProgressIndicationType.RETRYING);
    }
}

リトライは延々と行われるわけではなく、MAX_NUM_RETRIES-1回試行されます。
# MAX_NUM_RETRIESはデフォルト5なので、4回はリトライ処理されます

mNumRetriesSoFarの値が上限であるMAX_NUM_RETRIESに達していない場合は、再度
startRetryTimerがコールされます。

以上が機内モード設定時の緊急呼発信シーケンスです。
緊急呼発信シーケンスを解析すると
  • 緊急呼発信時は3rdパーティによる電話番号編集の影響を受けない
  • リトライ処理(機内モード解除→発信)の範例
を垣間見ることができました。

中々目にすることのない動作ではあるものの極めて重要な処理です。
3rdパーティは緊急呼発信処理を妨げないように徹底しなければなりません。

以上です。
2012/03/15

Android:音声発信までのシーケンスと音声発信イベントのフック

音声発信時はやや複雑なシーケンスを経て行われます。
また、3rdパーティアプリが音声発信シーケンスに介入する余地も残されています。

今回は音声発信シーケンスを見ていきます。

音声発信時の大まかな処理は下記の流れになります。

android.intent.action.CALLが音声発信を開始する直接のIntentではないことがわかります。
音声発信には通常呼以外にSIP, OTAやvoicemail、緊急呼といった様々な種類があります。
Androidは3rdパーティに緊急呼発信を許可していませんが、通常呼は許可しています。
これらの仕様を満たすために音声発信シーケンスはやや複雑化しています。

今回は音声発信シーケンスの中でも"通常呼発信"をメインに扱います。
次回は"緊急呼発信"をメインに扱います。
いずれもIntent,BroadcastIntentやHandlerを巧みに利用したテクニックです。

# ソースコードの抜粋は重要箇所のみ抜き出しています
# コードはver.4.0.3ベースです。

●音声発信の開始
ダイヤル系アプリがaction.CALLで音声発信するとOutgoingCallBroadcasterが反応します。
その名の通り音声発信ブロードキャストを送信するための透明なActivityです。
OutgoingCallBroadcasterにはアクティビティ別名が存在します。
下記定義となります(一部省略)。

・com.android.phone AndroidManifest.xml
<activity android:name="OutgoingCallBroadcaster"
        android:permission="android.permission.CALL_PHONE"
        android:theme="@android:style/Theme.NoDisplay">
    <!-- CALL action intent filters, for the various ways
         of initiating an outgoing call. -->
    <intent-filter>
        <action android:name="android.intent.action.CALL" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="tel" />
    </intent-filter>
</activity>

<activity-alias android:name="EmergencyOutgoingCallBroadcaster"
        android:targetActivity="OutgoingCallBroadcaster"
        android:permission="android.permission.CALL_PRIVILEGED"
        android:theme="@android:style/Theme.NoDisplay">
    <intent-filter>
        <action android:name="android.intent.action.CALL_EMERGENCY" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="tel" />
    </intent-filter>
</activity-alias>

<activity-alias android:name="PrivilegedOutgoingCallBroadcaster"
        android:targetActivity="OutgoingCallBroadcaster"
        android:permission="android.permission.CALL_PRIVILEGED">
    <intent-filter>
        <action android:name="android.intent.action.CALL_PRIVILEGED" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="tel" />
    </intent-filter>
</activity-alias>

それぞれのActivityは
  • OutgoingCallBroadcaster : 通常呼
  • EmergencyOutgoingCallBroadcaster : 緊急呼
  • PrivilegedOutgoingCallBroadcaster : 上記2つのハイブリッド
となります。3rdパーティアプリは緊急呼を発信できないのでOutgoingCallBroadcaster
以外気にする必要はないでしょう。

OutgoingCallBroadcasterの処理を追っていきます。

・com.android.phone OutgoingCallBroadcaster.java
protected void onCreate(Bundle icicle) {
    // Intentから電話番号取得
    String number = PhoneNumberUtils.getNumberFromIntent(intent, this);

    // SIPや緊急呼の判定を経てのBroadcastIntent作成処理
    Intent broadcastIntent = new Intent(Intent.ACTION_NEW_OUTGOING_CALL);
    if (number != null) {
        broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
    }
    PhoneUtils.checkAndCopyPhoneProviderExtras(intent, broadcastIntent);
    broadcastIntent.putExtra(EXTRA_ALREADY_CALLED, callNow);
    broadcastIntent.putExtra(EXTRA_ORIGINAL_URI, uri.toString());
    if (DBG) Log.v(TAG, "Broadcasting intent: " + broadcastIntent + ".");
    sendOrderedBroadcast(broadcastIntent, PERMISSION, new OutgoingCallReceiver(),
                         null,  // scheduler
                         Activity.RESULT_OK,  // initialCode
                         number,  // initialData: initial value for the result data
                         null);  // initialExtras
}

送信されるブロードキャストはandroid.permission.PROCESS_OUTGOING_CALLSパーミッシ
ョン付きです。
これにより、ブロードキャスト受信側はパーミッションを宣言する必要があります。

resultDataには電話番号が格納されています。
ブロードキャスト種別はオーダーで、全てのレシーバが電話番号を処理した結果を
OutgoingCallReceiverが受け取る仕組みです。

ここで、ACTION_NEW_OUTGOING_CALLのブロードキャストIntentを投げることで、
3rdパーティアプリが音声発信をフックすることを可能にしています。

ブロードキャストの結果はOutgoingCallReceiverに届きます。
OutgoingCallReceiverの処理を追っていきます。

・com.android.phone OutgoingCallBroadcaster$OutgoingCallReceiver.java
public void doReceive(Context context, Intent intent) {
    // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData
    // is used as the actual number to call. (If null, no call will be
    // placed.)
    number = getResultData();

    if (number == null) {
        if (DBG) Log.v(TAG, "CALL cancelled (null number), returning...");
        return;
    }

    startSipCallOptionHandler(context, intent, uri, number);
}

ブロードキャストの結果を受信すると、ユーザが電話番号を編集している可能性がある
ため、緊急呼判定が再度行われます。
通常呼と判断されればSipCallOptionHandlerを呼び出し、処理を継続します。

・com.android.phone OutgoingCallBroadcaster.java
private void startSipCallOptionHandler(Context context, Intent intent,
            Uri uri, String number) {
    Intent selectPhoneIntent = new Intent(ACTION_SIP_SELECT_PHONE, uri);
    selectPhoneIntent.setClass(context, SipCallOptionHandler.class);
    selectPhoneIntent.putExtra(EXTRA_NEW_CALL_INTENT, newIntent);
    selectPhoneIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    if (DBG) Log.v(TAG, "startSipCallOptionHandler(): " +
            "calling startActivity: " + selectPhoneIntent);
    context.startActivity(selectPhoneIntent);
    // ...and see SipCallOptionHandler.onCreate() for the next step of the sequence.
}

SipCallOptionHandlerはSIP通話の判定処理を行いますが、通常呼として処理すべき場合
は、そのまま通常呼処理を継続します。

#Android2.3以降よりSIP通話が標準サポートされました。

・com.android.phone SipCallOptionHandler.java
public void onCreate(Bundle savedInstanceState) {
    // SIP通話判定を経て...
    setResultAndFinish();
}

private void setResultAndFinish() {
    runOnUiThread(new Runnable() {
        public void run() {
            if (mUseSipPhone && mOutgoingSipProfile == null) {
                // SIP通話時処理
                return;
            } else {
                // Woo hoo -- it's finally OK to initiate the outgoing call!
                PhoneApp.getInstance().callController.placeCall(mIntent);
            }
            finish();
        }
    }
}

placeCallまでたどり着きました。これでようやく音声発信処理が開始されます。
(かなりのコードを省略しました。"Woo hoo"のコメントをみても発信処理チェックの
長いことがわかります...)

次に、placeCallの中を追っていきます。

・com.android.phone CallController.java
public void placeCall(Intent intent) {
    // 様々な状態チェックや前準備を経て、RILへ発信要求
    CallStatusCode status = placeCallInternal(intent);

    // 音声発信画面を表示
    mApp.displayCallScreen();
}

・com.android.phone PhoneApp.java
void displayCallScreen() {
    startActivity(createInCallIntent());
}

/* package */ static Intent createInCallIntent() {
    Intent intent = new Intent(Intent.ACTION_MAIN, null);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
            | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
            | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
    intent.setClassName("com.android.phone", getCallScreenClassName());
    return intent;
}

placeCallInternal(intent)はPhoneUtils.placeCall()を呼び出し、最終的にRILへ発信
要求を送信します。
発信要求を送信した後は、音声発信中であることをユーザへ伝えるためInCallScreen
アクティビティ(getCallScreenClassNameの戻り値)を起動してUIの表示に至ります。

3rdパーティアプリが音声発信シーケンスで関与するのは
android.intent.action.CALLとandroid.intent.action.NEW_OUTGOING_CALLでしょう。

繰り返しとなりますが、
3rdパーティアプリは緊急呼を発信することができませんが、
NEW_OUTGOING_CALLのブロードキャストインテントを受信することで、音声発信シーケンス
に介入することができます。


音声発信シーケンスを解析すると、
  • ブロードキャストによる他アプリ連携と拡張性の確保。
  • Intentによるモジュール間の連携
を垣間見ることができました。

コードを見るとわかりますが、3rdパーティアプリがNEW_OUTGOING_CALLを受信する場合は
細心の注意が必要です。
間違ってResultDataに意図しない値(nullや全く異なる電話番号)を代入してしまうと、
全く音声発信できない、あるいは予期しない電話番号に発信されてしまうことになります。
# Broadcastの性質上、ユーザはどのアプリが悪さをしているのか判断するのが困難です

OutgoingCallBroadcasterには、音声発信に関する様々な仕様がコードコメントとして記載
されています。
気になる方には一見の価値があります。


次回は、今回省略した"緊急呼"についてです。

緊急呼は極めて特殊な存在です。
この番号が発信されないという事態は、生命の危険に関わることなので絶対に避ける必要
があります。
そのため、緊急呼発信シーケンスは通常呼発信シーケンスとは異なるルートを通り、
あらゆる手段を使って発信を試みるように作られています。

3rdパーティアプリが緊急呼に関わることはほとんどないため、仕様に関する知識はそ
れほど役に立たないかもしれませんが、「あらゆる手段を使って発信を試みる」のロジッ
クは一見の価値があります。

以上です。
2012/03/14

Android:Preference項目のレイアウトファイル

Preference項目のレイアウトファイルと、参照までのシーケンスを調べてみました。
テーマによってレイアウトを変化させるテクニックを垣間見ることができます。


開始地点はPreference情報を注入するPreferenceFragment.addPreferencesFromResource
です。(PreferenceActivityも同じですが非推奨APIのため記載しません)

●android.preference.PreferenceFragment
public void addPreferencesFromResource(int preferencesResId) {
    requirePreferenceManager();

    setPreferenceScreen(mPreferenceManager.inflateFromResource(getActivity(),
            preferencesResId, getPreferenceScreen()));
}

PreferenceManagerのinflateFromResourceを使用してPreferenceを構築していきます。

●android.preference.PreferenceManager
public PreferenceScreen inflateFromResource(Context context, int resId,
        PreferenceScreen rootPreferences) {
    // Block commits
    setNoCommit(true);

    final PreferenceInflater inflater = new PreferenceInflater(context, this);
    rootPreferences = (PreferenceScreen) inflater.inflate(resId, rootPreferences, true);
    rootPreferences.onAttachedToHierarchy(this);

    // Unblock commits
    setNoCommit(false);

    return rootPreferences;
}


インフレートで生成されるPreferenceのコンストラクタを拝見。
なにやらcom.android.internal.R.attr.preferenceStyleのStyleを指定しています。

●android.preference.Preference
public Preference(Context context, AttributeSet attrs) {
    this(context, attrs, com.android.internal.R.attr.preferenceStyle);
}


参照先のpreferenceStyleの定義を拝見。
各テーマごとに定数定義されており、参照するstyleもそれぞれで異なります。

●framework/base/core/res/res/values/themes.xml
<style name="Theme">
    <item name="preferenceStyle">@android:style/Preference</item>

<style name="Theme.Holo">
    <item name="preferenceStyle">@android:style/Preference.Holo</item>


参照先のstyleの定義を拝見。
各スタイルが参照するレイアウトは下記の通り。

●framework/base/core/res/res/values/styles.xml
<style name="Preference">
    <item name="android:layout">@android:layout/preference</item>
</style>

<style name="Preference.Holo">
    <item name="android:layout">@android:layout/preference_holo</item>
</style>


各レイアウトはframework/base/core/res/res/layout配下に格納されています。
上記から、テーマによってPreferenceが参照するレイアウトが異なることがわかりました。
また、テーマによってレイアウトの参照先を変更可能であることもわかりました。

以上です。

2012/03/13

Android:match_parentとfill_parent

match_parent,fill_parentについて、Android2.2以降はfill_parentが非推奨となったため、
match_parentを使用する必要がありますが、match_parentとfill_parentに名前以外の違い
が本当に無いのか気になったので調査してみました。

結論から言うと、名前以外に違いはありませんでした。

match_parentとfill_parentは、ViewGroupの内部クラスLayoutParamsに定義されています。
public static class LayoutParams {
    /**
     * Special value for the height or width requested by a View.
     * FILL_PARENT means that the view wants to be as big as its parent,
     * minus the parent's padding, if any. This value is deprecated
     * starting in API Level 8 and replaced by {@link #MATCH_PARENT}.
     */
    @SuppressWarnings({"UnusedDeclaration"})
    @Deprecated
    public static final int FILL_PARENT = -1;

    /**
     * Special value for the height or width requested by a View.
     * MATCH_PARENT means that the view wants to be as big as its parent,
     * minus the parent's padding, if any. Introduced in API Level 8.
     */
    public static final int MATCH_PARENT = -1;
どちらも同じ値でした。
やはり単に名前変更だけのリファクタリングだったようです。

以上です。
2012/03/11

Android:セキュリティの確保と耐タンパー性

Androidのセキュリティは話題に事欠きません。
Android端末は基本的にユーザ権限で動作するように作られています(最小権限の原理です)
しかし、Androidの脆弱性をついてroot権限を奪取する攻撃が存在します。
既に発売されている端末でもこれは同様で、今現在のAndroidはroot権限の奪取を防ぐこと
ができていません。

このことから、アプリ開発においては「root権限が奪取された環境で動作する」あるいは、
「アプリは解析される」という前提で、アプリの耐タンパー性を考える必要があります。

下記はデータをAESの方式で暗号化する簡単なサンプルです。
private static final String SECRET_KEY_SRC = "ABCDEFGHIJKLMNOP";
private static final String SECRET_IV_SRC = "QRSTUVWXYZabcdef";

@Override
protected void onResume() {
    super.onResume();

    String target = System.getProperty("yuki.target", "test");
    Logger.d("Target=" + target);
    byte[] encrypted = encrypt(target.getBytes());
    Logger.d("Encrypt=" + new String(encrypted));
    byte[] decrypted = decrypt(encrypted);
    Logger.d("Decrypt=" + new String(decrypted));
}

private byte[] encrypt(byte[] target) {
    if (target == null || target.length <= 0) {
        return new byte[0];
    }

    // create SecretKey
    byte[] keySrc = SECRET_KEY_SRC.getBytes();
    SecretKeySpec key = new SecretKeySpec(keySrc, "AES");

    // create IV
    byte[] ivSrc = SECRET_IV_SRC.getBytes();
    IvParameterSpec iv = new IvParameterSpec(ivSrc);

    try {
        Cipher encrypt = Cipher.getInstance("AES/CBC/PKCS5Padding");
        encrypt.init(Cipher.ENCRYPT_MODE, key, iv);
        return encrypt.doFinal(target);
    } catch (Exception e) {
        throw new RuntimeException("Encrypt error.");
    }
}

private byte[] decrypt(byte[] target) {
    if (target == null || target.length <= 0) {
        return new byte[0];
    }

    // create SecretKey
    byte[] keySrc = SECRET_KEY_SRC.getBytes();
    SecretKeySpec key = new SecretKeySpec(keySrc, "AES");

    // create IV
    byte[] ivSrc = SECRET_IV_SRC.getBytes();
    IvParameterSpec iv = new IvParameterSpec(ivSrc);

    try {
        Cipher decrypt = Cipher.getInstance("AES/CBC/PKCS5Padding");
        decrypt.init(Cipher.DECRYPT_MODE, key, iv);
        return decrypt.doFinal(target);
    } catch (Exception e) {
        throw new RuntimeException("Encrypt error.");
    }
}

上記コードはセキュリティを十分に確保できるものでしょうか?この答えは一概には言えません。
上記のようなコードで十分なケースもあれば、全く暗号化の必要がないケースもあるからです。

耐タンパー性を考える上で意識しなければならないことについて、私なりの考えを本稿に
まとめました。
※本稿は完全な安全性を保証するものではありません。
※あくまでも参考として頂きますようお願いいたします。

0.耐タンパー化する前に...
耐タンパー化を実施する前に下記を熟考する必要があります
・そもそも耐タンパー化が必要か?
・耐タンパー化が必要な場合、求められる強度はどの程度か?

1つめは重要です。
悪意あるユーザにアプリのデータを抜き取られても全く問題のないアプリをつくるにはど
うすればよいのでしょうか?答えは非常にシンプルです。
「攻撃者にとって価値のある情報を持たなければよい」のです。
これについては思っている以上に検討の余地が残っているはずです。
アプリが必要以上にデータを持たなくて済む方法はないか?検討することが重要です。

2つめについては「あなたのアプリに必要なセキュリティ強度は?」
という質問に置き換えることもできます。
例えば、目覚まし時計のアラーム設定時刻が入ったDBとクレジットカード番号が入ったDB。
間違いなく後者の方が高いセキュリティを求められます。
このように、セキュリティ保護の対象となるデータによって求められるセキュリティ強度
は異なるのです。

あなたはアプリを攻撃する"悪意あるユーザ"の立場にあると仮定します。
目覚まし時計のアラーム設定時刻を盗むために端末をroot化してapkを抜き取り、逆コン
パイルした上で難読化されたコードを解析するでしょうか?おそらくしないでしょう。
データを盗むのにもコストが掛かります。データを盗んで得られるメリットよりデメリッ
トの方が大きいと攻撃される危険性は下がり、ソフトウェアは安全になります。
計算量的安全性の上に成り立っている暗号化の考え方も似たようなものです。

あなたはアプリを難読化するだけで十分に安全を担保できるのか?データの暗号化が必要
なのか?求められる強度はどの程度なのかを把握する必要があります。

また、耐タンパー化処理はアプリのパフォーマンスに影響することを忘れてはいけません。
いくらセキュリティを確保したとしても、パフォーマンスが劣悪では誰もそのアプリを欲
しいとは思わないからです。

これらを踏まえた上で耐タンパー化について考えていきます。


1.耐タンパー化:難読化(JavaとJNI)
以前投稿しましたが、Javaコードはapkさえ手に入れば逆コンパイルで抜き取ることが可
能です。可能であればJavaコード上で秘匿情報を扱わないのが得策です。
AndroidではJNI経由でネイティブ実装する仕組みがサポートされています。
ネイティブコードは機械語変換されるため、コード解析の難易度が飛躍的にあがります。


2.秘匿情報の寿命
ネイティブ側で秘匿情報を扱う際にも、メモリダンプには注意する必要があります。
鍵情報のようなデータを扱う場合、これをメモリ上に展開するとメモリダンプによって盗
まれる危険性があります。
そのため、秘匿情報をメモリ上に展開する時間は可能な限り短くするのが得策です。


3.名前への配慮
例えば「secret_key.bin」といった名前のファイルがあった場合を考えます。
このような名前は「これは秘密鍵です。盗むのであればこのファイルでしょう」
と攻撃者にヒントを与えているようなものです。

ファイル名だけではありません。コード上の変数名やメソッド・関数名、クラスファイル名、
データベースのフィールド名、"AES"といったような文字列も解読の手がかりとなります。
攻撃者へのヒントとなるような情報は可能な限り伏せた方が良いでしょう。
これらは難読化として知られ、いくつかはツールがサポートしてくれますが、ファイル名
などツールのサポート外となるものは、うっかり忘れがちです。


4.数字への配慮
128,256といった数字にも配慮しましょう。
「名前への配慮」と同じく解読の手がかりとなります。

アプリ解析でa, b, cの3ファイルが見つかり、どれか1つが鍵データであると仮定します。
それぞれのファイルサイズが
  • a - 40,000bit
  • b - 128bit
  • c - 8,000bit
である場合、ファイルbが鍵データである可能性が高くなります。
これは、鍵長に128bit, 160bit, 256bitがよく使用されるためです。

鍵データには余分なデータ(実行時には無視するデータ)を付加する等の工夫が必要です。


1~4は「攻撃者からデータを隠蔽し、解読を困難にする方法」であることがわかります。
暗号化といった高度なものから、相手の心理や行動をついたものまで耐タンパー化の方法
は様々です。
ここで、再び最初に示したコードを見ると様々な脆弱性が見えてくると思います。

自分のアプリに求められるセキュリティ強度を把握した上で、1~4それぞれの対策の
必要or不必要を判断、適用してセキュリティの向上に努めていくことが肝心です。

以上です。

2012/03/09

Android:Broadcastを受信できないアプリのSTOP状態

Android3.1以降、アプリケーションに新たなステータス"STOP"が追加されました。
アプリインストール後、一度も起動されていないアプリはSTOP状態となります。
システムアプリを除いて、STOP状態のアプリはBroadcastIntentを受け取れない場合があり
ます。
# 概要はhttp://developer.android.com/sdk/android-3.1.htmlで紹介されています。

今回は、アプリのSTOP状態について調査します。

調査は下記のコードで実施しています。
BOOT_BOMPLETEを受け取るだけのレシーバをAndroidManifestで静的に登録しているアプリです。

AndroidManifest.xml
<receiver
        android:name="MyBroadcastReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

yuki.broadcast.MyBroadcastReceiver
public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        android.util.Log.e("yuki", "yuki onReceive");
    }
}

上記で作成したアプリで下記手順を実施し、ブロードキャストインテントを受け取れてい
るかどうかをログ("yuki onReceive")から判断します。
# 手順は上から順番に実施。

1)端末にアプリを新規にインストールしてブロードキャスト送信
adb install apk/BroadcastTest.apk
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応なし/インストール直後はSTOP状態


2)再起動して再度ブロードキャスト送信
adb shell reboot
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応なし/STOP状態は再起動しても変わらない


3)アプリ起動後にプロセスkillした後にブロードキャスト送信
adb shell am start -n yuki.broadcast/.BroadcastTestActivity
adb shell kill <アプリPID>
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応あり/一度アプリ起動すれば、今まで通りブロードキャスト受信は可能


4)端末再起動後にブロードキャスト送信
adb shell reboot
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応あり/一度でもアプリ起動していれば、再起動後でも受信可能


5)アプリを更新した後にブロードキャスト送信
adb install -r apk/BroadcastTest.apk
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応あり/アプリ更新後も、継続して受信可能


6)アプリをアンインストール→再インストール後にブロードキャスト送信
adb uninstall yuki.broadcast
adb install apk/BroadcastTest.apk
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応なし/再インストール後は再度STOP状態になる


7)FLAG_INCLUDE_STOPPED_PACKAGESを指定してブロードキャスト送信
# パッケージ再インストール
adb uninstall yuki.broadcast
adb install apk/BroadcastTest.apk

adb shell am broadcast -a android.intent.action.BOOT_COMPLETED --include-stopped-packages
結果:反応あり/FLAG_INCLUDE_STOPPED_PACKAGESを付与すればSTOP状態でも受信可能


8)FLAG_INCLUDE_STOPPED_PACKAGESで受信後にFLAG_EXCLUDE_STOPPED_PACKAGES指定で送信
# パッケージ再インストール
adb uninstall yuki.broadcast
adb install apk/BroadcastTest.apk

# FLAG_INCLUDE_STOPPED_PACKAGES付与したブロードキャスト送信
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED --include-stopped-packages

# FLAG_EXCLUDE_STOPPED_PACKAGES付与したブロードキャスト送信
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED --exclude-stopped-package
結果:反応あり/FLAG_INCLUDE_STOPPED_PACKAGES指定で受信さえすればSTOP状態は解除
される


まとめ:
  • 一度でもアプリを起動すれば、再起動後でも受信可能
  • アプリの更新でSTOP状態にはならない
  • アプリの再インストールはSTOP状態になる
  • FLAG_INCLUDE_STOPPED_PACKAGESで一度でも受信すればSTOP状態は解除される

Android4.x以降でもこれは有効です。
「ブロードキャストを受信できない」といった不具合が出た場合、STOP状態となっていな
いか確認したほうがよいでしょう。



●ソースコード解析
本動作・仕様についてシステム側のソースコードを解析してみます。
ブロードキャストインテント送信開始からのコードを追いました。

IntentResolverでIntentを処理するコンポーネントを探索。
com.android.server.IntentResolver.buildResolveList(...)

final boolean excludingStopped = intent.isExcludingStopped();
final int N = src != null ? src.size() : 0;
boolean hasNonDefaults = false;
int i;
for (i=0; i<N; i++) {
    F filter = src.get(i);
    int match;
    if (debug) Slog.v(TAG, "Matching against filter " + filter);

    if (excludingStopped && isFilterStopped(filter)) {
        if (debug) {
            Slog.v(TAG, "  Filter's target is stopped; skipping");
        }
        continue;
    }

excludingStoppedはIntentフラグのFLAG_EXCLUDE_STOPPED_PACKAGESがONである場合True。
isFilterStoppedは対象がSTOP状態である場合True。
両方Trueの場合は、Filter targe(アプリ)がSTOP状態と判断してIntent処理しない。


対象のSTOP状態を調べるメソッドを追ってみます。
com.android.server.pm.PackageManagerService.ActivityIntentResolver.isFilterStopped(...)
@Override
protected boolean isFilterStopped(PackageParser.ActivityIntentInfo filter) {
    PackageParser.Package p = filter.activity.owner;
    if (p != null) {
        PackageSetting ps = (PackageSetting)p.mExtras;
        if (ps != null) {
            // System apps are never considered stopped for purposes of
            // filtering, because there may be no way for the user to
            // actually re-launch them.
            return ps.stopped && (ps.pkgFlags&ApplicationInfo.FLAG_SYSTEM) == 0;
        }
    }
    return false;
}

コメントを見るとシステムアプリはSTOP状態でもIntentの受信は可能のようです。
ps.stoppedの設定元を追っていくとcom.android.server.pm.Settings.readStoppedLPw()
の下記にたどり着きます。
String tagName = parser.getName();
if (tagName.equals("pkg")) {
    String name = parser.getAttributeValue(null, "name");
    PackageSetting ps = mPackages.get(name);
    if (ps != null) {
        ps.stopped = true;
        if ("1".equals(parser.getAttributeValue(null, "nl"))) {
            ps.notLaunched = true;
        }
    } else {
        Slog.w(PackageManagerService.TAG, "No package known for stopped package: " + name);
    }

なにやらXMLをパースして、pkgタグのname属性に記載されているパッケージのnl属性値が
1であればパッケージSTOP状態としています。

パースしているXMLは/data/system/packages-stopped.xmlのようです。
yuki.broadcastアプリを一度も起動していないと下記のようなXMLの内容になります。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<stopped-packages>
<pkg name="yuki.broadcast" nl="1" />
   ... その他色々
</stopped-packages>

一度でも起動するとpkgタグは削除されるので、
ps.stopped = true;
となるルートは通らなくなります。

これらのことから、下記手順を試してみます。
# パッケージ再インストール
adb uninstall yuki.broadcast
adb install apk/BroadcastTest.apk

# packages-stopped.xmlを取得
adb pull /data/system/packages-stopped.xml packages-stopped.xml

# ...
#  packages-stopped.xmlからyuki.broadcastのtagを削除
# ...

# 編集したpackages-stopped.xmlで置き換える
adb pull packages-stopped.xml /data/system/packages-stopped.xml

# ブロードキャスト送信
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応あり/STOP状態はpackages-stopped.xmlで管理されている

以上です。

Android:Eclipseが端末を認識しない、adbが繋がらない場合の対処方法

adbが突然つながらなくなったり、端末(やエミュレータ)を認識しなくなる場合があります。
Eclipseからadb接続しつつ、コマンドプロンプトからもadb接続をしていると発生しやすい
気がします。

adbが繋がらない場合は一度adb接続を切断して、再接続すると回復する場合があります。

adb kill-server
adb devices


コマンド発行時に下記のエラーが表示された場合


C:\dev\android-sdk-windows\platform-tools>adb devices
* daemon not running. starting it now on port 5037 *
ADB server didn't ACK
* failed to start daemon *
error: cannot connect to daemon

複数のadbプロセスが起動している場合があるので、これらを一度全て強制終了します。


稀にadb.exeが起動→終了→起動→終了とループしている場合があります。
この場合、一瞬ですが起動している時に素早くプロセスを強制終了します。

プロセスを強制終了した後、再度adbに接続すると回復する場合があります。

adb kill-server
adb devices

以上です。

Android:ログコードをProguardで削除する


Android標準で用意されているandroid.util.Logクラスは便利なロギング機能を提供して
くれますが、ログ出力の度にTAGを指定するのは面倒です。
また、アプリ内でフォーマットを統一したい場合はLogクラスをラップする独自のロガー
を用意することはよくあると思います。

今回下記のようなロガーを作成しました。
特徴は、オプションで出力フォーマットにファイル名と行数を含めることができます。
# 出力される情報はロガーの呼出し元となります。
# 情報はスタックトレースから取り出すので、コールスタックを意識する必要があります

package yuki.proguard;

import android.util.Log;

public abstract class Logger {
    private static final String LOG_TAG = "YourAppName";

    private static boolean DUMP_CALLER_INFO = true;
    // Log format: <log message> @<file name>:<line number>
    private static final String CALLER_INFO_FORMAT = " @%s:%s";

    public static void v(String log) {
        if (!isLoggable(Log.VERBOSE)) {
            return;
        }
        printLog(Log.VERBOSE, log, null);
    }

    public static void d(String log) {
        if (!isLoggable(Log.DEBUG)) {
            return;
        }
        printLog(Log.DEBUG, log, null);
    }

    public static void i(String log) {
        if (!isLoggable(Log.INFO)) {
            return;
        }
        printLog(Log.INFO, log, null);
    }

    public static void w(String log) {
        if (!isLoggable(Log.WARN)) {
            return;
        }
        printLog(Log.WARN, log, null);
       
    }

    public static void w(String log, Throwable err) {
        if (!isLoggable(Log.WARN)) {
            return;
        }
        printLog(Log.WARN, log, err);
       
    }

    public static void e(String log) {
        if (!isLoggable(Log.ERROR)) {
            return;
        }
        printLog(Log.ERROR, log, null);
       
    }

    public static void e(String log, Throwable err) {
        if (!isLoggable(Log.ERROR)) {
            return;
        }
        printLog(Log.ERROR, log, err);
       
    }

    private static boolean isLoggable(int level) {
        return Log.isLoggable(LOG_TAG, level);
    }

    private static void printLog(int level, String log, Throwable err) {
        StringBuilder msg = new StringBuilder(50);
        msg.append(log)
            .append(getErrorStackTrace(err))
            .append(getCallerInfo());

        Log.println(level, LOG_TAG, msg.toString());
    }

    private static String getErrorStackTrace(Throwable err) {
        if (err == null) {
            return "";
        }

        StringBuilder msg = new StringBuilder(400);
        msg.append("\n");
        msg.append(Log.getStackTraceString(err));
        return msg.toString();
    }

    private static String getCallerInfo() {
        if (DUMP_CALLER_INFO) {
            return "";
        }

        StackTraceElement[] stacks = new Throwable().getStackTrace();
        if (stacks == null || stacks.length < 4) {
            // スタックトレースの4番目を呼出し元情報として扱います。
            //   ex) Calling stack is..
            //   stack[0] : Logger.getCallerInfo()
            //   stack[1] : Logger.printLog()
            //   stack[2] : Logger.i()
            //   stack[3] : CallerClass.xxx()
            // stack[3]にあたるクラスが呼出し元情報として出力されます。
            return "";
        }

        StackTraceElement stack = stacks[3];
        return String.format(CALLER_INFO_FORMAT,
                stack.getFileName(), stack.getLineNumber());
    }
}

利用する側は下記のようなコードで実行されます。

public class ProguardTestActivity extends Activity {
// ....
        Logger.d("Debug dump Test");
        Logger.v("Debug dump Test");
// ....
}

これを実行すると、下記のログが出力されます。

Debug dump Test @ProguardTestActivity:3
Debug dump Test @ProguardTestActivity:4


●ログの抱える問題と対応策
ログはデバッグ時には非常に重要な情報ですので、出力の内容は正確かつ価値のあるもの
である必要がありますが、下記のような問題が付きまといます。
  • ログ情報からアプリの秘匿情報が知られる恐れがある
  • ログ出力が原因でパフォーマンスが低下する

開発者に価値のあるログ情報は、悪意のあるユーザにとっても価値ある情報になるケース
があります。

Android初期バージョンでは、電話番号がログ情報に流れる問題があり、悪意のあるアプリ
がログから電話番号を盗むことも可能でした。(現在、この問題は修正されています)
また、Logcatのストリームを監視することでユーザプライバシーに関わる情報も盗めて
しまう可能性があります。
# クレジットカード番号や氏名・体重・年齢等をログ出力するのは避けましょう

また、ログの出力はアプリパフォーマンスにも影響します。
例えば、上記のLoggerクラスでログ出力を抑止(isLoggable==false)しても、ログ情報の
文字列は生成されるため、不要なオーバーヘッドが生まれます。

これら2つの問題を解決するにはProguardを利用するのが便利です。

今回の例ではproguard.cfgに下記定義を追加します。

-assumenosideeffects class yuki.proguard.Logger {
    public static *** v(...);
    public static *** d(...);
    public static *** i(...);
}

これで、Loggerのv,d,iメソッドはProguard処理で削除されるようになります。
ログ情報を生成する為の文字列も生成されないため、不要なオーバーヘッドが発生する
心配もありません。

実際にProguard適用後のapkを逆コンパイルしてみましょう。

【適用前コード】
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    Logger.d("Debug dump Test");
    Logger.v("Debug dump Test  ");
}


【適用後コード】
public void onCreate(Bundle bundle)
{
    super.onCreate(bundle);
    setContentView(0x7f030000);
}

見事にログコードが削除されています。


●Proguardの副作用
Proguardには副作用もあります。
今回の例ですと、難読化によりスタックトレース情報が変化し、正しく呼出し元情報が出
力されなくなります。
これは難読化でメソッドがインライン展開されて、スタックトレースの4番目が呼出し元
クラスであることが保証されなくなるためです。
# そもそもスタック要素が4つ未満になることもあります

難読化後もスタックトレースの順序を保つのは困難です。
# 全てを難読化対象外とすれば可能ですが.....

以上です。

2012/03/08

Android:難読化ツールのProguardを適用する


Proguardは
  • ソースコードの難読化
  • 未使用のコード部分の削除
を支援するツールです。
リバースエンジニアリングを困難にし、apkのサイズを小さくする効果があります。

難読化はAndroidのセキュリティを考える上でよく出てくるキーワードです。
今回はProguard適用方法と、どれほど難読化されるのかを見ていこうと思います。

●project.propertiesの編集
Proguardの適用は非常に簡単です。
※ただし、ProguardはADT8.0以上、SDK-r8以上の環境が必要です。

EclipseでAndroidプロジェクトを作成すると、プロジェクトルートの直下に
project.propertiesファイルが自動生成されています。


Proguardを有効にするためには、このプロパティファイルを編集します。

今回指定するProguardコンフィグファイルは、プロジェクトルート直下に自動生成され
るproguard.cfgを指定しましょう。
下記の一行をプロパティファイルの最終行に追加します。
proguard.config=proguard.cfg

これでProguard機能がproguard.cfgの内容に従って動作するようになりました。

# 2012/03/09 追記
apk生成時に「Conversion to Dalvik format failed with error 1」というエラーが発生して
正しく処理できない場合があります。その場合は下記のサイトが助けになるでしょう。
Androidアプリ開発情報まとめブログ

# 2012/05/18 追記
ADT17以降は、proguard.cfgの扱いが若干変わります。
詳しくは下記ページを参照下さい。
http://d.hatena.ne.jp/bs-android/20120325/1332662384


●Proguard機能を試す
Proguardはプロジェクトを右クリックした時に表示される[AndroidTools]>
[Export Signed Application Package]からapkを生成すると適用されます。

以上でProguardの適用は完了なのですが、実感が湧きづらいので本当に難読化されている
のか確認してみましょう。

今回、下記のようなコードを組みました。
これをProguardで難読化してみようと思います。
public class ProguardTestActivity extends Activity {
 private CountDownLatch mLatch = new CountDownLatch(2);

 @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Logger.d("Debug dump Test");
        Logger.e("Debug dump Test");
        android.util.Log.e("ProguardTest", "test dump");
    }

    @Override
    protected void onResume() {
     super.onResume();

     new AsyncTask<CountDownLatch, Void, Void>() {
      @Override
      protected Void doInBackground(CountDownLatch... latch) {
       try {
        latch[0].await();
       } catch (InterruptedException e) {
        // N.O.P
        Logger.d("wake up.");
       }
       Logger.d("hello.");

       return null;
      }
  }.execute(mLatch);

  Handler handler = new Handler();
  handler.post(new Runnable() {
   @Override
   public void run() {
    mLatch.countDown();
   }
  });
  handler.postDelayed(new Runnable() {
   @Override
   public void run() {
    mLatch.countDown();
   }
  }, 2000);
    }
}

難読化されているかの確認は、前回の「Android:Windowsでapkを逆コンパイルする方法
を利用して、難読化処理有の.apkと難読化処理無の.apkとで比較することにします。

難読化有/無のapkから上記のコード部分を抽出したものが下記になります。

●難読化無し
public ProguardTestActivity()
{
    mLatch = new CountDownLatch(2);
}

public void onCreate(Bundle bundle)
{
    super.onCreate(bundle);
    setContentView(0x7f030000);
    Logger.d("Debug dump Test");
    Logger.e("Debug dump Test  ");
    Log.e("ProguardTest", "test dump");
}

protected void onResume()
{
    super.onResume();
    AsyncTask asynctask = new AsyncTask() {

        protected volatile transient Object doInBackground(Object aobj[])
        {
            return doInBackground((CountDownLatch[])aobj);
        }

        protected transient Void doInBackground(CountDownLatch acountdownlatch1[])
        {
            try
            {
                acountdownlatch1[0].await();
            }
            catch(InterruptedException interruptedexception)
            {
                Logger.d("wake up.");
            }
            Logger.d("hello.");
            return null;
        }

        final ProguardTestActivity this$0;

      
        {
            this$0 = ProguardTestActivity.this;
            super();
        }
    };

    CountDownLatch acountdownlatch[] = new CountDownLatch[1];
    acountdownlatch[0] = mLatch;
    asynctask.execute(acountdownlatch);
    Handler handler = new Handler();
    handler.post(new Runnable() {

        public void run()
        {
            mLatch.countDown();
        }

        final ProguardTestActivity this$0;

      
        {
            this$0 = ProguardTestActivity.this;
            super();
        }
    });

    handler.postDelayed(new Runnable() {

        public void run()
        {
            mLatch.countDown();
        }

        final ProguardTestActivity this$0;

      
        {
            this$0 = ProguardTestActivity.this;
            super();
        }
    }, 2000L);
}

private CountDownLatch mLatch;

ほとんど原型をとどめています。
これであればソースコードの解読はかなり容易にできそうです。


●難読化有り
public class ProguardTestActivity extends Activity
{

    public ProguardTestActivity()
    {
        a = new CountDownLatch(2);
    }

    static CountDownLatch a(ProguardTestActivity proguardtestactivity)
    {
        return proguardtestactivity.a;
    }

    public void onCreate(Bundle bundle)
    {
        super.onCreate(bundle);
        setContentView(0x7f030000);
        yuki.proguard.a.a("Debug dump Test");
        yuki.proguard.a.b("Debug dump Test");
        Log.e("ProguardTest", "test dump");
    }

    protected void onResume()
    {
        super.onResume();
        b b1 = new b(this);
        CountDownLatch acountdownlatch[] = new CountDownLatch[1];
        acountdownlatch[0] = a;
        b1.execute(acountdownlatch);
        Handler handler = new Handler();
        handler.post(new c(this));
        handler.postDelayed(new d(this), 2000L);
    }

    private CountDownLatch a;
}

難読化処理無しに比べると非常に読みづらいコードになっています。
よく見ると、AsyncTaskの内部クラスがcという名前のクラスに置き換えられています。
この時、Proguardによってc.javaが新規生成されており難読化を促進していることがわか
ります。
他にもLoggerがaというクラス名に変更されており、一見すると何のクラスかわかりません。

しかし、ログの内容や2000といった直値まではさすがに難読化できていません。
このことから暗号鍵のバイナリを定数等で宣言するのがどれだけ危険で、容易に盗まれて
しまうかが想像できます。


●Proguardの注意点
Proguardには注意点もあります。
Proguardはソースコード(メソッド名・クラス名も含む)を変更するため、リフレクション
等をしたコードでは、目的のクラスやメソッド名が見つからないといった不具合に繋がる
恐れがあります。
また、Proguardは不要と判断したソースコードの削除も行うため、JINやリフレクション
による参照のみのメソッドやクラスは、誤って不要と判断されるケースがあります。
こういった事態を防ぐために、proguard.cfgで難読化やソースの削除を抑止する方法が提
供されています。

以上です。