2012/10/22

Android:JBで追加されたREAD_EXTERNAL_STORAGE

●はじめに

JellyBeanでREAD_EXTERNAL_STORAGE権限が追加されました。
READ_EXTERNAL_STORAGE権限は外部SDカードの読み込みを制御するものです。

JBにおいて、この権限は将来有効化される"予約された権限"という位置付けです。
(SDカードを読み込むアプリは、将来READ_EXTERNAL_STORAGE権限が必要になるということです)
そのため、JBでのREAD_EXTERNAL_STORAGE権限は何の効力も持ちません。

宣言を見ても、テスト用であることがわかります。
<!-- Allows an application to read from external storage -->
<permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:permissionGroup="android.permission-group.DEVELOPMENT_TOOLS"
    android:label="@string/permlab_sdcardRead"
    android:description="@string/permdesc_sdcardRead"
    android:protectionLevel="normal" />
http://tools.oesf.biz/android-4.1.2_r1.0/xref/frameworks/base/core/res/AndroidManifest.xml#692

将来のためにも、SDカードの読み込みが必要なアプリはREAD_EXTERNAL_STORAGE権限を要求するようにしましょう。


●動作確認

JBでは、開発者オプションでSDカードの読み込み時にREAD_EXTERNAL_STORAGE権限を必須にすることが可能です。
これを活用すれば、将来READ_EXTERNAL_STORAGE権限が必須になった場合の予行練習が可能です。

これを設定するには、
 [設定]>[開発者向けオプション]>[SDカードの保護]
にチェックを付けます。 ※Nexus7の場合は[USBストレージの保護]になっています。
これで、SDカードの読み込み時にREAD_EXTERNAL_STORAGE権限がチェックされるようになります。

# 2012/10/22現在、最新のエミュレータ(SDK Platform 16 Rev.2)ではこの確認を実施できない可能性があります。
# この環境では何度試してもエラーが発生しませんでした。

あとは、下記のようなコードでSDにアクセスすれば動作確認できます。
File sdFile = new File(Environment.getExternalStorageDirectory(),
        "/memo.txt");
try {
    FileInputStream fis = new FileInputStream(sdFile);
    fis.read();
    fis.close();  // TODO: move finally section.
} catch (Exception e) {
}
READ_EXTERNAL_STORAGE権限を持っていないと、下記のようなエラーが発生します。
java.io.FileNotFoundException: /storage/sdcard0/memo.txt: open failed: EACCES (Permission denied)
  at libcore.io.IoBridge.open(IoBridge.java:416)
  at java.io.FileInputStream.<init>(FileInputStream.java:78)
  at com.example.sdcardread.MainActivity.onResume(MainActivity.java:28)
  ...
Caused by: libcore.io.ErrnoException: open failed: EACCES (Permission denied)
  at libcore.io.Posix.open(Native Method)
  at libcore.io.BlockGuardOs.open(BlockGuardOs.java:110)
  at libcore.io.IoBridge.open(IoBridge.java:400)
  ... 18 more

ただし、WRITE_EXTERNAL_STORAGE権限を持っていると、READ_EXTERNAL_STORAGE権限を持っていなく
ても、SDカードの読み込みは可能です。
これは、WRITE_EXTERNAL_STORAGE権限がREAD_EXTERNAL_STORAGE権限を内包しているためです。

この辺りはPackageParserの128行目にSplitPermissionとして定義されています。
new PackageParser.SplitPermissionInfo[] {
  // READ_EXTERNAL_STORAGE is always required when an app requests
  // WRITE_EXTERNAL_STORAGE, because we can't have an app that has
  // write access without read access.  The hack here with the target
  // target SDK version ensures that this grant is always done.
  new PackageParser.SplitPermissionInfo(android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
    new String[] { android.Manifest.permission.READ_EXTERNAL_STORAGE },
    android.os.Build.VERSION_CODES.CUR_DEVELOPMENT+1),
http://tools.oesf.biz/android-4.1.2_r1.0/xref/frameworks/base/core/java/android/content/pm/PackageParser.java#128


●そのほか

GBでのSDマウントポイントは /mnt/sdcard が主流でした。
ICSでは /mnt/sdcard/external_sd。
しかし、JBでは /storage/sdcard0/ が主流となりつつあるようです。

この辺はキャリアによって様々です。
動作確認の際、試験ファイルのpush先には注意しましょう。

以上です。

2012/10/15

Android:xhdpiリソースしか持たないアプリが抱える問題


Androidのdrawableに関する情報、特にマルチスクリーン対応に関する話題で
わざわざ各density毎にリソース(画像等)を作らなくても、
xhdpiリソースのみアプリに持たせて、より低いdensityのhdpiやmdpi向けには
xhdpiリソースを縮小したもので代用すれば良いのではないか?
という声を聞くことがあります。

今回はこの考えのメリットとデメリットについての考察です。
※明確な答えを知っている訳ではないので間違いがあるかもしれません。

たしかにxhdpiリソースを他densityのリソースとして代用すれば
  • 画像を複数用意する手間が省ける
  • apkサイズを小さく出来る
  • 画像の管理がチョッとは楽
等々、メリットがいくつかあります。

他にも、mdpi画像をxhdpi端末で表示すると拡大表示されることになり、画像の品質が低下しますが、
xhdpi画像をmdpi端末で表示すれば縮小表示されることになり、画像の品質低下は先程より軽微に思えます。

これでは、mdpiやhdpiリソースを用意する意味があるのか疑いたくなりますが、
xhdpiリソースだけでは不都合があるため今の仕組みがあるはずです。


●mdpi端末でxhdpi画像を表示する場合の品質劣化

Developersサイトには下記の記載があります。
スケーリングによる画像の拡縮によって、画像が不自然に見えることがある。
これを防ぎ、美しい画像の表示を保証するために各画面密度用の画像を用意すべきである。
(引用元)http://developer.android.com/guide/practices/screens_support.html#support

mdpi端末でxhdpi画像を表示した場合、品質はどの程度劣化するのでしょうか?
1つ実験してみました。

■実験内容
細かい網目状の左画像をxhdpiリソースとして用意。

これを各Densityの端末で
 そのままのサイズで表示した場合 or どのDensityでも同じ見た目に揃えた場合
どのように表示されるか試してみます。


端末はエミュレータを使用し、表示結果をDDMSのスクリーンショットで取得します。
各Density毎のエミュレータ設定は下記。
MDPI:
・Skin: HVGA
・Density: 160(mdpi)

HDPI:
・Skin: WVGA800
・Density: 240(hdpi)

XHDPI:
・Skin: WXGA720
・Density: 320(xhdpi)

画像を表示するため、レイアウトファイルに下記ImageViewを定義。
<ImageView
    android:id="@+id/test"
    android:layout_width="@dimen/img_width"
    android:layout_height="@dimen/img_height"
    android:src="@drawable/test" />
また、後者の"どのDensityでも同じ見た目に揃えるケース"では、
各Densityリソースに下記dimenリソースを用意します。
・values-mdpi
<resources>
    <dimen name="img_width">50dp</dimen>
    <dimen name="img_height">50dp</dimen>
</resources>
・values-hdpi
<resources>
    <dimen name="img_width">75dp</dimen>
    <dimen name="img_height">75dp</dimen>
</resources>
・values-xhdpi
<resources>
    <dimen name="img_width">100dp</dimen>
    <dimen name="img_height">100dp</dimen>
</resources>

■実験結果
各Densityでの表示結果が下。
低いDensityだと網目が潰れてしまうことがわかります。
これは"そのままのサイズで表示した場合"、"どのDensityでも同じ見た目に揃えた場合"
どちらでも同じです。

・実行結果


■まとめ
風景や人物画像等、多少圧縮されても表示上問題ない画像でも
小さなアイコンや細かなUI部品といった1pxの潰れが無視できない画像の場合は
ちゃんと各Density毎の画像を用意するほうがよさそうです。


●mdpi端末でxhdpiを表示する場合のパフォーマンス低下

mdpi端末で結局圧縮・縮小されるxhdpiリソースを読み込むのは明らかに無駄です。
mdpi画像を用意すれば、圧縮・縮小の必要はなく無駄にサイズの大きなデータを読み込む必要がありません。

では、mdpi端末でxhdpi画像を読み込んだ場合のパフォーマンス低下はどれ程でしょうか?

■実験内容
画像のmdpi版とxhdpi版を準備。

これをmdpi端末で読み込み、
 xhdpi版のみ準備したケース or mdpi版も準備したケース
で表示速度を計測します。

計測区間はsetContentViewの前後としています。
また、キャッシュによる高速化を回避するため、測定の都度プロセスをkillします。


■実験結果
測定の結果、
xhdpi版のみ準備したケースでは約513msec
mdpi版も準備したケースでは約239msec
となりました。

倍以上の差がでました。
xhdpiの画像サイズはmdpiの倍なので妥当な結果と言えそうです。


■まとめ
mdpi端末でxhdpiリソースを表示するとパフォーマンスが低下します。
大きなxhdpi画像をmdpi端末で読み込ませることは、無駄なオーバーヘッドに繋がり得策ではなさそうです。


●全体まとめ

xhdpi画像を低いDensity端末で表示すると画像が潰れることがある。
低いDensity端末でxhdpi画像を読み込むと、適切なDensityの画像を読み込む場合と比べてパフォーマンスが悪くなる。

安易に"xhdpiリソースのみ用意する方法"を選択すべきではないようです。
上記の悪影響を踏まえた上で、実際に動作を確認して選択する必要がありますね。

また、apkのサイズを減らしたいのであれば.9.pngやShapeによる描画等のテクニックが用意されています。

UIはアプリの印象を決める大きな要素です。
綺麗で洗練されたUIはユーザを強く惹きつけますね。
品質に大きく関わる部分でもありますのでこの辺は注意したいところです。

以上です。

2012/10/10

Android:packages.xmlとデフォルト起動Activity

●デフォルトActivityの選択

ほとんどの場合、共有アクションに反応できるActivityは複数存在します。
ActivityChooserにより、ユーザはどのActivityで処理するかを選択します。

ICS以前とJB以降とで、そのUIは若干変わりましたが基本は同じです。
ユーザはActivityの選択を"次回以降にも反映する(Always)"か"今回限りのものとする(Just once)"かを決めることができます。
Activity選択画面

ユーザがActivityの選択を次回以降にも反映させようとした場合。
次回以降、同じIntentが投げられると、ActivityChooserは表示されず選択したActivityが起動されます。
これは"デフォルト起動設定"と呼ばれる機能で、都度ユーザが選択動作をする手間を省くものです。

本稿では、デフォルト起動設定の対象となったActivityをデフォルト起動Activityと記載します。


●packages.xml

デフォルト起動Activityの情報は/data/system/packages.xmlに永続化されます。
com.android.server.pm.Settingsはこれをパースし、Activity起動を制御する情報として扱います。

packages.xmlの中を見ていきます。
デフォルト起動Activityの情報はpreferred-activitiesタグ配下に列挙されます。
工場出荷状態だと空タグになっていることがほとんどです。
<preferred-activities />

この状態で、adb shell am start -a android.intent.action.MAINコマンドを投げます。
するとaction.MAINに反応する多くのActivityが一覧表示されます。
ここで[Always]を選択してデフォルト起動Activityを決定します。

10秒程放置してからpackages.xmlをpullするとpreferred-activitiesに変化があります。
<preferred-activities>
  <item name="com.android.systemui/.DreamsDockLauncher" match="100000" set="82">
    <set name="com.android.settings/.inputmethod.InputMethodAndSubtypeEnablerActivity" />
    <set name="com.android.settings/.Settings$AccessibilitySettingsActivity" />
    <set name="com.android.systemui/.DreamsDockLauncher" />
    ...省略...
    <filter>
      <action name="android.intent.action.MAIN" />
      <cat name="android.intent.category.DEFAULT" />
    </filter>
  </item>
</preferred-activities>

なにやらIntentFilterらしき情報とコンポーネントらしき情報が追加されています。
順を追ってそれぞれの内容をみていきます。


●preferred-activities

・<preferred-activities>
デフォルト起動Activity情報のブロックです。
preferred-activitiesは0以上のitem要素を内包します。

・<item>
デフォルト起動Activityの情報です。
ActivityChooserでユーザが[Always]指定した情報がここに格納されます。
itemタグには、1つのIntentと、関連する複数のコンポーネント情報が登録されます。

・<item>@name
name属性はデフォルト起動Activityのコンポーネント名です。
<filter>にマッチするIntentを検知すると、ここで指定されたActivityが起動します。

・<item>@match
<Filter>で定義されるIntentFilterのマッチング種別値です。
この値はIntentFilterクラスの下記定数で定義されます。
  0x0100000 :MATCH_CATEGORY_EMPTY
  0x0200000 :MATCH_CATEGORY_SCHEME
  0x0600000 :MATCH_CATEGORY_TYPE
   ...等々
例えば、
・IntentFilterに<data>を持たない場合はMATCH_CATEGORY_EMPTY
・<data android:scheme>を持つ場合はMATCH_CATEGORY_SCHEME
・<data android:mimeType>を持つ場合はMATCH_CATEGORY_TYPE
といった具合です。

ちなみに、SchemeとTypeの両方を持つ場合はTypeが優先される等、優劣も存在します。

・<item>@set
Activity(<set>)の総数です。

・<set>
<Filter>で定義されるIntent情報に関連するActivity情報です。
Activityが複数ある場合は、複数の<set>が定義されます。

・<set>@name
<Filter>で定義されるIntent情報に関連するActivityのコンポーネント名です。

・<filter>
デフォルト起動するActivityのIntent情報です。
内包する子要素(actionやcategory)はIntentFilterのそれと同等なので省略します。

packages.xmlの中を読めば、
 "Intentに反応できるActivityが複数あった場合にデフォルト起動されるであろうActivity"
の手がかりとなります。


●packages.xmlが更新されるタイミング

デフォルト起動Activityの情報はpackages.xmlを参照して判断されますが、
Intentが発行される度に参照するわけではありません。
packages.xmlをパースした情報はメモリ上にキャッシュし、通常はこちらを参照します。

packages.xmlのpreferred-activities情報が更新される契機をいくつか挙げます。
・Intentに対するActivityのデフォルト起動情報を変更した
・アプリケーション情報画面等から[デフォルト起動情報]をクリアした
・新しいアプリケーションがインストールされた


●packages.xmlの鮮度

メモリ上にキャッシュされている情報とpackages.xmlの情報に差異が生じる時間が存在します。
PackageManagerServiceはメモリ上に展開したpackages.xmlの情報に変更が発生すると、
およそ10秒後にファイルシステム上のpackages.xmlを更新します。
更新タイミングにディレイがあるため、packages.xmlの古い状態が存在します。
# Activityのデフォルト起動設定を変更した直後に電池抜きを行うと
# デフォルト起動設定の変更が再起動後に反映されていないことに気づくでしょう。


●関連クラス

packages.xmlに関連するクラスをいくつか挙げます。

デフォルト起動Activity情報クラス。
  • com.android.server.pm.PreferredActivity
  • com.android.server.PreferredComponent

Intentのマッチング関連
  • android.content.IntentFilter.matchData
どのマッチング種別値になるのかがここで決まります。

packages.xmlのI/O
  • com.android.server.pm.Settings.readLPw
  • com.android.server.pm.Settings.writeLPw
com.android.server.pm.Settings.mSettingsFilenameがpackages.xmlのファイルパスになります。

packages.xmlの更新タイミング
  • com.android.server.pm.PackageManagerService.scheduleWriteSettingsLocked
HandlerへWRITE_SETTINGSメッセージを10000msディレイで送信していることが確認できます。

以上です。
2012/09/24

Android:隔離プロセス上でServiceを実行する~isolatedProcess~


JellyBeanからServiceを隔離プロセス上で実行できるようになりました。

Serviceを隔離プロセス上で実行するにはAndroidManifestのserviceタグでisolatedProcess属性にtrueを指定します。
# デフォルト値はfalse
http://developer.android.com/guide/topics/manifest/service-element.html#isolated
<service ...
    android:isolatedProcess="true" />

●PIDとUID

実際にこのサービスを実行すると、呼出し元とは別のPIDで起動しているのがわかります。
# ps
USER      PID   PPID  VSIZE  RSS   WCHAN    PC         NAME
...
u0_a47    1068  37    171612 34304 ffffffff 40033a40 S yuki.test.isolateservice
u0_i5     1083  37    170200 28668 ffffffff 40033a40 S yuki.test.isolateservice
...
隔離プロセス上ではUIDも異なります。

isolatedProcess=false(同プロセス)でサービスを呼び出した場合
caller  UID=10047 / PID=1198
service UID=10047 / PID=1198

isolatedProcess=true(隔離プロセス)でサービスを呼び出した場合
caller  UID=10047 / PID=1068
service UID=99006 / PID=1083
隔離プロセスのUIDには99000~99999が割り当てられます。
参考:android.os.Process.FIRST_ISOLATED_UID / LAST_ISOLATED_UID


●隔離プロセスの制限

隔離プロセス上で実行できる動作には厳しい制限があります。

【動作制限の例】
  • ContentResolverを操作できない
  • BroadcastIntentを送信できない
  • BroadcastReceiverを登録できない
  • 自プロセスのメモリ情報を取得できない
  • その他色々...

この辺の動作制限は、主にActivityManagerServiceのenforceNoIsolatedCaller()でチェックしています。
http://tools.oesf.biz/android-4.1.1_r1.0/xref/frameworks/base/services/java/com/android/server/am/ActivityManagerService.java#2240
隔離プロセスから制限有りのAPIを呼ぶとエラーが返されます。

エラーメッセージ「Isolated process not allowed to call ***」

・BroadcastIntentの送信時
java.lang.SecurityException: Isolated process not allowed to call broadcastIntent
  ...
  at android.content.ContextWrapper.sendBroadcast(ContextWrapper.java:312)
  at yuki.test.isolateservice.IsolatedService.onHandleIntent(IsolatedService.java:14)

・ContentResolver経由でクエリ発行時
java.lang.SecurityException: Isolated process not allowed to call getContentProvider
  ...
  at android.content.ContentResolver.query(ContentResolver.java:313)
  at yuki.test.isolateservice.IsolatedService.onHandleIntent(IsolatedService.java:18)

●隔離プロセス上にあるサービスとの通信

隔離プロセス上にあるサービスとの通信はstartやbindといった基本APIに限られます。
バインド状態にあるサービスの独自APIをコールすることはできません。

サービス接続時に渡されるIBinderにはBinderProxyインスタンスが格納されています。
private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        LocalBinder binder = (LocalBinder)service;  // ClassCastException発生!
    }
そのため、サービスインスタンスが取得できません。


●隔離プロセスの使いどころ

ドキュメントとして、使用すべきシーンやガイドラインが見当たりませんが、、、
最小権限の原理と、サービスが基本的にバックグラウンドで動作するという特性から、
セキュリティに関連するアップデートと思われます。
隔離プロセスを応用したベストプラクティスがあれば新たに記事を投稿しようと思います。

以上です。

2012/09/02

Android:TimingLoggerで処理間隔をログで出力する

TimingLoggerというクラスがあります。

このクラスを使用すると、処理間隔をわかりやすくログ出力できます。
実際にコードと出力イメージをみたほうが理解できます。

サンプルコード
TimingLogger timings = new TimingLogger("tag", "method");
// work a
timings.addSplit("work a");
// work b
timings.addSplit("work b");
// work c
timings.addSplit("work c");
timings.dumpToLog();
ログ
D/tag     ( 3459): method: begin
D/tag     ( 3315): method:      5 ms, work a
D/tag     ( 3315): method:      3 ms, work b
D/tag     ( 3315): method:      12 ms, work c
D/tag     ( 3315): method: end, 20 ms
TimingLoggerはスレッドセーフに設計されていません。
TimingLoggerで使える主なメソッドは下記。

TimingLogger(String tag, String label)
コンストラクタ。
dumpToLogでの出力結果に、ログタグtagと計測対象のメソッド名labelを設定します。

addSplit(String splitLabel)
計測に"マーカー"となるラベルを設定します。
dumpToLogでの出力結果に、前回マーカーポイントからの経過時間を表示させます。

dumpToLog()
計測した結果をログ出力します。

reset()
計測情報を全てリセットします。
addSplitで設定したマーカー情報もリセットされます。
コンストラクタで指定したtagとlabel情報はそのまま残ります。

reset(String tag, String label)
計測情報に加えて、コンストラクタで設定したログタグとメソッド名情報をtag,labelで上書きます。

参考:http://developer.android.com/reference/android/util/TimingLogger.html

以上です。

2012/08/30

Android:CancellationSignalで処理を中断する

●はじめに

JellyBeanで"処理のキャンセル要求"を表現するCancellationSignalクラスが追加されました。

下記のようなシーンで利用されています。
  • ローダの処理を中断する。 
  • データベースへのクエリ要求を中断する。
「前者はLoaderのcancelLoadと違うのか?」って疑問を持つ方もいると思います。

ローダについては、CancellationSignalの仕組みを取り込む形で内部設計が変更されました。
この変更より、ローダ利用側がしないといけないことは特にありません。

一般的に利用されるシーンとしては、後者の"データベースへのクエリ要求の中断"でしょう。
例えば、下記のような利用が考えられます。
  • クエリを発行したものの、結果返却される前にアプリ終了したのでクエリをキャンセルしたい。
  • クエリを発行したものの、(ロック解放待ち等により)一定時間応答がないのでクエリをキャンセルしたい。
これまでは、一度発行したクエリは投げっぱなしで、中断する術がありませんでした。

(ContentProvider側をゴリゴリカスタムすればできそうですけど...)
JellyBean以降、CancellationSignalが導入されたことでこれが可能となりました。

●CancellationSignal概念

CancellationSignalは"処理の中断要求"を表現するクラスです。
このクラスは、キャンセル状態の管理とリスナーへのコールバックを持つシンプルなものです。
CancellationSignalはあくまでもキャンセル"要求"です。
このキャンセル要求を受け入れるかどうかは、キャンセルされる処理側が判断します。

これは、AsyncTaskのそれと変わりありません。
AsyncTaskもキャンセルメソッドを持っていますが、本当にキャンセルされるかどうかは実装依存です。

独自のローダやContentProviderにキャンセル機能を実現したい場合でも同じです。
JB以前に作成したContentProviderにキャンセル要求を投げても上手く動きません。
キャンセル機能を実現したいならそれを組み込む必要があります。

データベースのクエリ要求では、少なくとも次のポイントでキャンセルできるよう実装されています。
このケースでは少なくとも次のポイントでキャンセル要求を確認しています。
  • ContentResolver/ContentProviderClientへのクエリ要求時 
  • データベースへの接続前と接続待ち中
  • クエリの実行前と実行中 
  • トランザクションの開始時と終了時
注意点として、標準で用意されているContentProviderでもキャンセル機能をサポートしていないものが存在します。
例えば通話履歴データを提供するCallLogProviderはこれをサポートしていません。
このようなContentProviderにもCancellationSignalを渡すことは可能ですが、結果無視されます。

●"キャンセル要求があるか?"

「キャンセル要求があるか?」を判断するには2通りの方法があります。
CancellationSignal.isCanceled()throwIfCanceled()です。
isCanceled()はキャンセル要求の有無(true/false)を返します。
throwIfCanceled()はキャンセルされている場合にandroid.os.OperationCanceledException(実行時例外)をスローします。

キャンセル要求をチェック/実行する側はthrowIfCanceled()を使用するケースが多いようです。
処理をキャンセルしたい場合は呼び出しもとにOperationCanceledExceptionをそのままスローします。
キャンセル要求を投げる側はisCanceled()を使うことがほとんどでしょう。

●CancellationSignalによるキャンセル要求をサポートする

ここでは、独自ContentProviderにCancellationSignalによるキャンセルをサポートさせてみます。
実装は非常に簡単。

まず、ContentProviderを継承したクラスで下記のqueryメソッドをオーバライドします。
ContentProvider.query (Uri, String[], String, String[], String, CancellationSignal)

public class MyContentProvider extends ContentProvider {
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder,
            CancellationSignal cancellationSignal) {
        ... 略 ...
    }
}

続いて、SQLiteDatabaseインスタンスに対して下記メソッド経由でCancellationSignalインスタンスを渡します。
SQLiteDatabase.query (boolean, String, String[], String, String[], String , String, String, String, CancellationSignal)

@Override
public Cursor query(Uri uri, String[] projection, String selection,
        String[] selectionArgs, String sortOrder,
        CancellationSignal cancellationSignal) {
    SQLiteDatabase db = mDataBaseHelper.getReadableDatabase();
    Cursor c = db.query(false, "test", projection,
            selection, selectionArgs, null, null, sortOrder, null,
            cancellationSignal);
    ... 略 ...
}

ContentProviderの呼出し元はContentResolverあるはContentProviderClient経由でCancellationSignalインスタンスを渡します。

・ContentResolver.query (Uri, String[], String, String[], String, CancellationSignal)
・ContentProviderClient.query (Uri, String[], String, String[], String, CancellationSignal)
mCancelSignal = new CancellationSignal();
Cursor cursor = getContentResolver().query(
        Uri.parse("content://yuki.mycontentprovider"),
        new String[] { "_id" }, null, null, null, mCancelSignal);
これで、SQLiteデータベースへのクエリ要求はキャンセル可能なものとして扱われます。
クエリ要求をキャンセルしたい場合は渡したCancellationSignalのcancelメソッドを呼び出します。
mCancelSignal.cancel();
キャンセル要求が受け入れられて処理中断されるとOperationCanceledException例外がスローされるのでこれをキャッチしましょう。
try {
    Cursor cursor = getContentResolver().query(
            Uri.parse("content://yuki.mycontentprovider"),
            new String[] { "_id" }, null, null, null, mCancelSignal);
} catch (OperationCanceledException ex) {
    // do something.
}
キャンセル要求のサポートは以上です。

ちなみにですが、AsyncTaskLoaderはCancellationSignalによるキャンセルをサポートしています。
キャンセル可能な範囲はバックグラウンド処理部分。
ちょっと気になるコメントがあったので引用します。
http://tools.oesf.biz/android-4.1.1_r1.0/xref/frameworks/base/core/java/android/content/AsyncTaskLoader.java#71

} catch (OperationCanceledException ex) {
    if (!isCancelled()) {
        // onLoadInBackground threw a canceled exception spuriously.
        // This is problematic because it means that the LoaderManager did not
        // cancel the Loader itself and still expects to receive a result.
        // Additionally, the Loader's own state will not have been updated to
        // reflect the fact that the task was being canceled.
        // So we treat this case as an unhandled exception.
        throw ex;
    }
CancellationSignalによるキャンセルが実行されたのに、Loaderとしてはキャンセル状態にない場合。
AsyncTaskLoaderはこれを異常として例外を再スローするようですね。

●キャンセル処理をサポートしていないContentProviderにCancellationSignalを渡したら...

クエリのキャンセルをサポートしていない or 古いContentProviderでもCancellationSignalを渡すことは可能です。
しかし、渡しても即破棄されるのでキャンセル要求は無視されます。

http://tools.oesf.biz/android-4.1.1_r1.0/xref/frameworks/base/core/java/android/content/ContentProvider.java#649
public Cursor query(Uri uri, String[] projection,
        String selection, String[] selectionArgs, String sortOrder,
        CancellationSignal cancellationSignal) {
    return query(uri, projection, selection, selectionArgs, sortOrder);
}


●お試し実装で...

以下はCancellationSignalの効果を確かめようと思って実施した内容です。
蛇足ではありますが、一部落とし穴があったので記載します。

キャンセル要求が本当に通るかを検証すべく、
Exclusiveロックされたデータベースにクエリを投げて、ロック解除待ち状態のクエリをキャンセルしてみようと思いました。

で、手軽にロックを掛けたかったのでsqlite3コマンドから直接
sqlite3> BEGIN EXCLUSIVE TRANSACTION;
としたのですが、これが誤り...

sqlite3コマンドから直接データベースを操作すると、期待通りの動作となりませんでした。
# ずっとロック解放待ちのままハマってしまう...

ちゃんとSQLiteDatabaseのbeginTransactionでロックを取得すれば上手くいきました。

----------
-------
---
以降はCancellationSignal調査中のメモ書き。あまり役に立たないネタです。
(といいつつ、キャンセル処理を自力で実装したい場合は参考になります)
「どうやってSQLiteデータベースはクエリを中断しているのか?」を追いました。

おおまかなシーケンスは下記(一部省略)。



下記はデータベースにクエリを発行した際のシーケンスを順に追っています。

1. SQLiteDatabaseに対してqueryを発行
android.database.sqlite.SQLiteDatabase.query(boolean, String, String[], String, String[], String, String, String, String, CancellationSignal)

2. query発行処理...
android.database.sqlite.SQLiteDatabase.queryWithFactory(CursorFactory, boolean, String, String[], String, String[], String, String, String, String, CancellationSignal)

3. SQLiteDirectCursorDriverのインスタンス化
android.database.sqlite.SQLiteDirectCursorDriver.SQLiteDirectCursorDriver(SQLiteDatabase, String, String, CancellationSignal)
SQLiteDirectCursorDriverはCancellationSignalをキャッシュする。
public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable,
        CancellationSignal cancellationSignal) {
    mDatabase = db;
    mEditTable = editTable;
    mSql = sql;
    mCancellationSignal = cancellationSignal;
}

4. SQLiteQueryとSQLiteCursorの生成
android.database.sqlite.SQLiteDirectCursorDriver.query(CursorFactory, String[])
public Cursor query(CursorFactory factory, String[] selectionArgs) {
    final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);
    ... 略 ...
        cursor = new SQLiteCursor(this, mEditTable, query);
    ... 略 ...
}

5. SQLiteQueryはCancellationSignalをキャッシュする
android.database.sqlite.SQLiteQuery.SQLiteQuery(SQLiteDatabase, String, CancellationSignal)
SQLiteQuery(SQLiteDatabase db, String query, CancellationSignal cancellationSignal) {
    super(db, query, null, cancellationSignal);
    mCancellationSignal = cancellationSignal;
}

6. SQLiteQueryは親クラスSQLiteProgramのコンストラクタを呼び出す
android.database.sqlite.SQLiteProgram.SQLiteProgram(SQLiteDatabase, String, Object[], CancellationSignal)
クエリ操作では、SQLiteSessionの準備を行う。
int n = DatabaseUtils.getSqlStatementType(mSql);
switch (n) {
    default:
        ... 略 ...
        db.getThreadSession().prepare(mSql,
                db.getThreadDefaultConnectionFlags(assumeReadOnly),
                cancellationSignalForPrepare, info);
        ... 略 ...
        break;
}

7. データベースコネクションの取得を開始
android.database.sqlite.SQLiteSession.prepare(String, int, CancellationSignal, SQLiteStatementInfo)
prepare処理前にキャンセル確認。
public void prepare(String sql, int connectionFlags, CancellationSignal cancellationSignal,
        SQLiteStatementInfo outStatementInfo) {
    ... 略 ...
    if (cancellationSignal != null) {
        cancellationSignal.throwIfCanceled();
    }
    ... 略 ...
}
さらに、SQLiteConnectionを取得
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
    mConnection.prepare(sql, outStatementInfo); // might throw
} finally {
    releaseConnection(); // might throw
}

8. コネクションプールにSQLiteConnection取得要求
android.database.sqlite.SQLiteConnectionPool.acquireConnection(String, int, CancellationSignal)

9. コネクションプールからSQLiteConnection取得できるまでウェイト
android.database.sqlite.SQLiteConnectionPool.waitForConnection(String, int, CancellationSignal)
ウェイト前にキャンセル確認
private SQLiteConnection waitForConnection(String sql, int connectionFlags,
        CancellationSignal cancellationSignal) {
        ... 略 ...
        // Abort if canceled.
        if (cancellationSignal != null) {
            cancellationSignal.throwIfCanceled();
        }
        ... 略 ...
}
ウェイト時にもキャンセルできるようにキャンセルリスナー前準備
cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
@Override
public void onCancel() {
        synchronized (mLock) {
            if (waiter.mNonce == nonce) {
                cancelConnectionWaiterLocked(waiter);
            }
        }
});
ウェイト開始...
for (;;) {
    ... 略 ...
    // Wait to be unparked (may already have happened), a timeout, or interruption.
    LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L);
    ... 略 ...
}
ウェイトが満了したらキャンセルリスナー解除
try {
    ... 略 ....
} finally {
    // Remove the cancellation listener.
    if (cancellationSignal != null) {
        cancellationSignal.setOnCancelListener(null);
    }
}

10. コネクションが取得できたらCursorWindowの準備
android.database.sqlite.SQLiteConnection.executeForCursorWindow(String, Object[], CursorWindow, int, int, boolean, CancellationSignal)
ネイティブへの問い合わせがあるので、ネイティブ実行中でもキャンセルできるよう前準備
try {
    final PreparedStatement statement = acquirePreparedStatement(sql);
        ... 略 ...
        attachCancellationSignal(cancellationSignal);
        try {
            final long result = nativeExecuteForCursorWindow(...);
            ... 略 ...
        } finally {
            detachCancellationSignal(cancellationSignal);
        }
        ... 略 ...
} catch (RuntimeException ex) {
                ... 略 ...

11. ネイティブ処理のキャンセルシグナルをアタッチ
android.database.sqlite.SQLiteConnection.attachCancellationSignal(CancellationSignal)
if (cancellationSignal != null) {
    cancellationSignal.throwIfCanceled();
    ... 略 ...
        // Reset cancellation flag before executing the statement.
        nativeResetCancel(mConnectionPtr, true /*cancelable*/);

        // After this point, onCancel() may be called concurrently.
        cancellationSignal.setOnCancelListener(this);
    ... 略 ...
}

ちなみに、SQLiteConnectionはCancellationSignal.OnCancelListenerをimplementsしている。
// CancellationSignal.OnCancelListener callback.
// This method may be called on a different thread than the executing statement.
// However, it will only be called between calls to attachCancellationSignal and
// detachCancellationSignal, while a statement is executing.  We can safely assume
// that the SQLite connection is still alive.
@Override
public void onCancel() {
    nativeCancel(mConnectionPtr);
}

12. キャンセルシグナルのデタッチ
android.database.sqlite.SQLiteConnection.detachCancellationSignal(CancellationSignal)
private void detachCancellationSignal(CancellationSignal cancellationSignal) {
    ... 略 ...
            // After this point, onCancel() cannot be called concurrently.
            cancellationSignal.setOnCancelListener(null);

            // Reset cancellation flag after executing the statement.
            nativeResetCancel(mConnectionPtr, false /*cancelable*/);
    ... 略 ...
}

13. SQLiteCursorはCancellationSignalをキャッシュしたSQLiteQueryをキャッシュする
android.database.sqlite.SQLiteCursor.SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)

これで、生成されたCursorがSQLiteDatabase.query()の戻り値になります。
他にもトランザクションの開始・終了時なんかにもキャンセル要求のチェックが入ります。

以上です。
2012/08/25

Android:ロングスクリーンと判断される基準

スクリーンがlongかnotlongかの判断は下記の式で決定されるようです。

  isLong = ((画面サイズの長い方*3)/5) >= (画面サイズの短い方-1))

例えば、800px×1280pxの端末の場合下記の判定式に置き換えられます。

  (1280*3)/5 >= 800-1

結果、notlongな端末と判断されます。


ただし、実際にはデコレーション領域を除いたサイズが参照されます。
ナビゲーションバー領域のサイズが減算されてた値が使用されます。
Nexus7だと、ナビゲーションバーの位置が画面の向きに依存します。

 port : 基本サイズ(x=800px, y=1280px), 参照サイズ(x=800px, y=1205px)


land : 基本サイズ(x=1280px, y=800px), 参照サイズ(x=1280px, y=736px)


該当するソースコードは下記です。

・com.android.server.wm.WindowManagerService.reduceConfigLayout()
// Is this a long screen?
if (((longSize*3)/5) >= (shortSize-1)) {
    // Anything wider than WVGA (5:3) is considering to be long.
    screenLayoutLong = true;
} else {
    screenLayoutLong = false;
}

"long, notlongは画面アスペクト比に依存するもので、画面の向きには依存しない"とDevelopersSiteに記載があります。
http://developer.android.com/guide/topics/resources/providing-resources.html
This is based purely on the aspect ratio of the screen (a "long" screen is wider). This is not related to the screen orientation.
でも、Nexus7のようなナビゲーションバーが画面の向きで変化する端末だと、
場合によってはこの前提が崩れてしまうのでは...

以上です。
2012/08/09

Android:JellyBeanで変更・非推奨となったメソッド・定数


JellyBeanで変更・非推奨となったメソッド・定数をいくつか抜粋。


●コンテンツの変更通知 -ContentObserver-

今までは監視したいURIの数だけObserverを用意する必要がありました。
JB以降は変更されたコンテンツURIも同時にObserverへ通知されるので、Observerを複数用意する必要がなくなります。

android.database.ContentObserver.dispatchChange(boolean)
非推奨となりました。
代わりにandroid.database.ContentObserver.dispatchChange(boolean, Uri)を使いましょう。


android.database.ContentObserver.dispatchChange(boolean, Uri)
新規追加のメソッドです。
引数Uriは変更のあったコンテンツのURIを指定します。


android.database.ContentObserver.onChange(boolean, Uri)
新規追加のメソッドです。
監視しているコンテンツに変更があった場合に呼ばれます。
引数Uriは変更のあったコンテンツのURIになります(nullの場合は"不明"を意味します)

JBより古い環境で互換性を維持したい場合、onChange(boolean)をオーバーライドする必要があります。
// Implement the onChange(boolean) method to delegate the change notification to
// the onChange(boolean, Uri) method to ensure correct operation on older versions
// of the framework that did not have the onChange(boolean, Uri) method.
@Override
public void onChange(boolean selfChange) {
    onChange(selfChange, null);
}

// Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
@Override
public void onChange(boolean selfChange, Uri uri) {
    // Handle change.
}


●データベース関連

メンテがちらほら。
acquireUnstableContentProviderClientは何気に影響範囲の広い修正です。
「Android:JellyBean以降のContentResolver.queryの挙動」

android.database.Cursor.deactivate()
非推奨となりました。
requery()が既に非推奨化されている(Honeycombからだったかな?)ので、これも非推奨です。
requery()の非推奨化時の忘れ物かな?


android.database.sqlite.SQLiteOpenHelper.setWriteAheadLoggingEnabled(boolean)
新規追加のメソッドです。
WALの有効/無効指定です。
引数booleanがtrueならenableWriteAheadLogging()、falseならdisableWriteAheadLogging()が実行されます。


android.content.ContentResolver.acquireUnstableContentProviderClient(Uri)
android.content.ContentResolver.acquireUnstableContentProviderClient(String)
新規追加のメソッドです。
信用できないContentProviderに対するクライアント(ContentProviderClient)を取得します。
過去の記事でも取り上げました。
「Android:高速化。ContentResolver?ContentProviderClient?」



●クエリ/リクエストのキャンセル -CancellationSignal-

コンテンツへのクエリがキャンセルできるようになりました。
これにより、コンテンツをロードするLoaderとそのサブクラスも同時に拡張されています。

また、"オペレーションのキャンセル"という抽象的な例外クラスOperationCanceledExceptionが追加されました。
クエリやローダをキャンセルした際にスローされるOperationCanceledExceptionにも注目です。

android.content.ContentResolver.query(Uri, String[], String, String[], String, CancellationSignal) 
新規追加のメソッドです。
cancellationSignalが指定できるようになり、クエリを中断することができます。
cancellationSignalについては追々調査。


android.content.AsyncTaskLoader.cancelLoadInBackground()
新規追加のメソッドです。
ローダの処理をキャンセルします。
cancelLoadInBackgroundはloadInBackground()処理が走っていない、あるいは既に終了している場合N.O.Pとするのが通例です。


android.content.AsyncTaskLoader.isLoadInBackgroundCanceled()
新規追加メソッドです。
ローダがキャンセル処理を受け付けたかどうかを確認します。


android.content.Loader.registerOnLoadCanceledListener(OnLoadCanceledListener<D>)
新規追加メソッドです。
ロード処理のキャンセルをlistenするリスナーを登録します。


android.content.Loader.unregisterOnLoadCanceledListener(OnLoadCanceledListener<D>)
新規追加メソッドです。
ロード処理のキャンセルをlistenするリスナー登録を解除します。


android.content.Loader.cancelLoad()
新規追加メソッドです。
ロード処理のキャンセルを試みます。


android.content.Loader.deliverCancellation()
新規追加メソッドです。
OnLoadCanceledListenerへのキャンセル通知を行います。


android.content.Loader.onCancelLoad()
新規追加メソッドです。
Loaderのサブクラスは、このメソッドをオーバーライドしてロード処理のキャンセル要求(cancelLoad)に応えます。



●その他

android.content.res.Configuration.ORIENTATION_SQUARE 
非推奨となりました。
⇒過去の記事で取り上げています「画面の向き:ORIENTATION_SQUARE


android.view.ActionProvider.onCreateActionView () 
非推奨となりました。
代わりにandroid.view.ActionProvider.onCreateActionView(MenuItem)を使いましょう。
JBより古い環境をサポートしたい場合は、このメソッドをオーバーライドして、有効なActionViewを返します。


android.view.ActionProvider.onCreateActionView(MenuItem)
新規追加のメソッドです。
JB以降はこのメソッドでActionViewを生成することが推奨されます。

以上です。

2012/08/06

Android:JellyBean以降のContentResolver.queryの挙動


事の発端は下記の投稿。
Issue 35610:  Why does getContentResover().query() acquire IContentProvider twice but leave the stable one only?  

JellyBeanで追加されたacquireUnstableProviderメソッド。
この変更がContentResolverやContentProviderClientにも波及している。

特にContentResolverのqueryやopenAssetFileDescriptorの変更は興味深い。
ContentResolver.java (r4.1.1)

まず、unstableなプロバイダを取得してクエリを発行。
qCursor = unstableProvider.query(uri, projection,
        selection, selectionArgs, sortOrder, remoteCancellationSignal);
取得したqCursorはCursorWrapperInnerでラップして、stableなプロバイダと紐付けさせている。
CursorWrapperInner wrapper = new CursorWrapperInner(qCursor,
        stableProvider != null ? stableProvider : acquireProvider(uri));
最後に、要らなくなったunstableなプロバイダをリリース。
} finally {
    if (unstableProvider != null) {
        releaseUnstableProvider(unstableProvider);
    }
なぜこんな処理をしているのか?
Issue 35610を投稿した方も色々疑問に思っているみたいですが、、、

プロバイダ側プロセスの死亡で道連れ死する期間を小さく抑えているのか?
はたまた、パフォーマンス的な問題なのか?

はっきりした答えがありませんが、
JellyBeanからContentResolver.queryの挙動が若干変わったことは覚えておいたほうが良さそうです。

以上です。
2012/08/03

Android:ContentProviderOperation.BuilderのAPI


情報源はDevelopersサイトのjavadoc。
ContentProviderOperation.Builder

●クラス概要

ContentProviderOperationを構築するビルダクラスです。
ContentProviderOperation.Builderのインスタンスは、newInsert/newUpdate/newDelete/newAssertQueryメソッドを呼び出して生成します。
newInsertで生成されたビルダはInsertタイプ、newUpdateで生成されたビルダはUpdateタイプといった具合になります。

withXXXメソッドではビルダにパラメータを追加するのに使います。
それぞれのメソッドは、ContentProviderOperation.Builderタイプが許可されているかをチェックします。
例えば、InsertタイプのビルダではwithSelection()を使用することはできません。

build()を呼ぶことで、ContentProviderOperationが構築・生成されます。


●public ContentProviderOperation build () 

ContentProviderOperation.BuilderからContentProviderOperationを生成します。


●public ContentProviderOperation.Builder withExpectedCount (int count) 

これを設定した場合、オペレーションによって影響をうける行数と引数countが一致しないとOperationApplicationExceptionをスローします。
このメソッドはupdate/delete/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withSelection (String selection, String[] selectionArgs) 

selectionに'?'が含まれている場合、selectionArgsの対応するIndexの値で置換されます。
selectionArgsのいずれかは、withSelectionBackReference(int, int)によって後方参照引数で上書きされる可能性があります。
このメソッドはupdate/delete/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withSelectionBackReference (int selectionArgIndex, int previousResult)

selectionArgsの後方参照値を指定します。
withSelection(String, String[])によって指定されたselectionArgIndexにある任意の値を上書きます。
このメソッドはupdate/delete/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withValue (String key, Object value) 

この値はwithValueBackReference(String, int)で指定された値で上書きされる可能性があります。
このメソッドはinsert/update/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withValueBackReference (String key, int previousResult) 

withValues(ContentValues)で指定された値よりも優先されます。
このメソッドはinsert/update/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withValueBackReferences (ContentValues backReferences)

key値はカラム名、vlaue値はpreviousResultのインデックス値。
withValues(ContentValues)で指定された値よりも優先されます。
このメソッドはupdate/delete/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withValues (ContentValues values) 

これはnullになる可能性があります。
この値はwithValueBackReference(String, int)で指定された値で上書きされる可能性があります。
あるいは、これより後に呼ばれるwithValues(ContentValues), withValue(String, Object)によっても上書きされます。
このメソッドはupdate/delete/assertタイプのビルダで使用可能です。

以上です。
2012/08/02

Android:ContentProviderOperationで後方参照を活用する


ContentProviderOperationには後方参照の機能が備わっています。
ここでの後方参照とは"クエリセット中における、前回の結果を参照する機能"です。

後方参照を使用すると、クエリ結果を別クエリの条件式で使用することができるようになります。
例えば下記のようなケースです。
  • クエリ1:page_numberが2のレコードを追加
  • クエリ2:クエリ1のレコードを親に持つ子レコードを追加
SQLに置き換えてみると下記のような感じでしょうか...
  • クエリ1:INSERT INTO tbl_A (name, page_number, parent_id) VALUES ('parent', 2, NULL);
  • クエリ2:INSERT INTO tbl_B (name, parent_id) VALUES ('child', 777 /* クエリ1で追加したレコードの_id値 */ );
クエリ2がクエリ1で追加したレコードの_idを必要としていることがわかると思います。

クエリ1の_id値はInsert文を発行してみないとわかりません。
こういう場合に、前回の結果を参照できる"後方参照"の使用を検討します。

ContentProviderOperation.Builderには後方参照を設定するためのメソッドが用意されています。
  • ContentProviderOperation.Builder.withSelectionBackReference(int, int) 
  • ContentProviderOperation.Builder.withValueBackReference(String, int)
  • ContentProviderOperation.Builder.withValueBackReference(ContentValues)

各メソッドの詳細は後述します。

実際にwithSelectionBackReferenceメソッドを使って後方参照してみます。
例えば、下記のようにオペレーションを構築したとします。
operations.add(ContentProviderOperation.newInsert(uri)
        .withValue("name", "test1").build());
operations.add(ContentProviderOperation.newUpdate(uri)
        .withValue("name", "test2").build());
operations.add(ContentProviderOperation.newInsert(uri)
        .withValue("name", "test3").build());
operations.add(ContentProviderOperation.newInsert(uri)
        .withValue("name", "test4").build());
operations.add(ContentProviderOperation.newUpdate(uri)
        .withValue("name", "test5")
        .withSelection("_id=?", new String[1])
        .withSelectionBackReference(0, 2)
        .build());
5つめのオペレーション構築で後方参照指定していますね。
selection、つまり抽出条件を"_id=?"としています。

withSelectionBackReferenceの第一引数はselectionArgIndex。
selectionで'?'パラメータとして渡されたインデックスを指定し、これを置換します。
何に置換されるかは第二引数のpreviousResultで決定されます。

previousResultには「何番目の結果を参照するか?」を指定します。
この場合は1~4つめまでのオペレーションで得られた結果のインデックス(0~3)を指定します。
previousResultに0を指定すれば1つめのオペレーションの結果が、1を指定すれば2つめのオペレーションの結果が得られます。
previousResultが指定する参照先は、applyBatchの戻り値でもあるContentProviderResultの配列、つまり結果セットそのものです。

参照された結果セット(ContentProviderResult配列)はどのような値を返すのか?後方参照でどのような値に置換されるのか?
これは、参照するContentProviderResultの種類(Insert or Update or Delete)によって変化します。
また、プロバイダのinsert/updateやdeleteがどのような戻り値を返すのかにも依存します。
通常、Insertは追加されたレコードのURI、Update/Deleteは影響を受けたレコード数が戻り値となりますね。
以降は、そのように実装されている前提で話を進めます。

後方参照の対象となる結果がInsertオペレーションによるものの場合。
後方参照の結果、Insertで追加されたレコードのID値が取得できます。

後方参照の対象となる結果がUpdate/Deleteオペレーションによるものの場合。
後方参照の結果、Update/Deleteで変更されたレコードの数が取得できます。

ContentProviderResultはInsertオペレーションによる結果はuriを保持し、Update/Deleteオペレーションによる結果はcountを保持すると説明しました。
しかし、後方参照ではInsertの結果をuriではなく、そのuriに含まれている(であろう)id値、つまり追加されたレコードの_id値が取得できます。
これは、後方参照用にContentProviderResultのuriを解析して、末尾の数字(つまり_id)を抽出しているためです。

実際に後方参照値を取り出すソースコードを見てみましょう。

・android.content.ContentProviderOperation.backRefToValue(ContentProviderResult[], int, Integer)
/**
 * Return the string representation of the requested back reference.
 * @param backRefs an array of results
 * @param numBackRefs the number of items in the backRefs array that are valid
 * @param backRefIndex which backRef to be used
 * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than
 * the numBackRefs
 * @return the string representation of the requested back reference.
 */
private long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
        Integer backRefIndex) {
    if (backRefIndex >= numBackRefs) {
        Log.e(TAG, this.toString());
        throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
                + " but there are only " + numBackRefs + " back refs");
    }
    ContentProviderResult backRef = backRefs[backRefIndex];
    long backRefValue;
    if (backRef.uri != null) {
        backRefValue = ContentUris.parseId(backRef.uri);
    } else {
        backRefValue = backRef.count;
    }
    return backRefValue;
}
if (backRef.uri != null) つまりInsertオペレーションによる結果(ContentProviderResult)である場合、uriから_idを抽出しています。
そうでない(つまりUpdate/Deleteオペレーションによる結果である)場合、countを返す仕様です。

ここで、もう一度オペレーション構築のコードに戻ります。
今度はコメントをつけてみました。
operations.add(ContentProviderOperation.newInsert(uri)  // previousResult 0番目
        .withValue("name", "test1").build());
operations.add(ContentProviderOperation.newUpdate(uri)  // previousResult 1番目
        .withValue("name", "test2").build());
operations.add(ContentProviderOperation.newInsert(uri)  // previousResult 2番目
        .withValue("name", "test3").build());
operations.add(ContentProviderOperation.newInsert(uri)  // previousResult 3番目
        .withValue("name", "test4").build());
operations.add(ContentProviderOperation.newUpdate(uri)  // previousResult 4番目
        .withValue("name", "test5")
        .withSelection("_id=?", new String[1])          // selectionArgIndex=0は_idに対する?
        .withSelectionBackReference(0, 2)               // selectionArgIndex=0, previousResult=2
5番目のオペレーションは後方参照の結果、下記のようなUpdate文になるでしょう。
WHEREで指定される_id値は、3つめのオペレーションで追加されたレコードIDの結果次第で変化します。
UPDATE test_table SET name = 'test5' WHERE _id = 777 /* 3つめのオペレーションで追加されたレコードID */ ;

以上が後方参照の基本的な動作です。

残る下記2つのメソッドについても基本的には同じ考え方です。
これらは、Insert/Updateで追加or更新したい値を指定するwithValue()の後方参照版です。
  • ContentProviderOperation.Builder.withValueBackReference(String, int)
  • ContentProviderOperation.Builder.withValueBackReference(ContentValues)

withValueBackReference(String, int)の2つ目の引数が前述のpreviousResultにあたります。
1つ目の引数は更新対象のカラム名です。

withValueBackReference(ContentValues)は、追加・更新したい値が複数カラムある場合に使います。
ContentValuesのkeyとvalueはwithValueBackReference(String, int)のそれと同じです。
ContentValuesのkeyが更新対象のカラム名、valuesが前述のpreviousResultにあたります。

下記はそのサンプル。
// nameの値がpreviousResult 2番目の結果で更新される。
ContentValues v = new ContentValues();
v.put("name", 2);
operations.add(ContentProviderOperation.newInsert(uri)
        .withValue("name", "test995")
        .withValueBackReferences(v).build());
後方参照は少しややこしいですが、
  • previousResultは結果セットの何番目を参照するかを指定する
  • 後方参照で得られる値はInsertの場合uri末尾の数字(_id値)、Update/Deleteは影響を受けたレコード数になる
ことを覚えておけば理解しやすいと思います。

以上です。
2012/08/01

Android:ContentProviderOperationとapplyBatch


データベースへのクエリをまとめて発行したい場合はapplyBatchの使用を検討します。

●applyBatchのメリット

・ContentResolver経由で1つずつクエリ実行するより高速
これは、ContentResolver.applyBatch内部でContentProviderClientを使用するためです。

引用:android.content.ContentResolver.applyBatch(String, ArrayList<ContentProviderOperation>)
public ContentProviderResult[] applyBatch(String authority,
        ArrayList<ContentProviderOperation> operations)
        throws RemoteException, OperationApplicationException {
    ContentProviderClient provider = acquireContentProviderClient(authority);
    if (provider == null) {
        throw new IllegalArgumentException("Unknown authority " + authority);
    }
    try {
        return provider.applyBatch(operations);
    } finally {
        provider.release();
    }
}
ContentProviderClientによる高速化については過去の記事で取り上げています。
高速化。ContentResolver?ContentProviderClient?

・トランザクション制御が容易
デフォルトではapplyBatchのアトミック性は保証されません。
これを保証するにはContentProviderのapplyBatchをオーバーライドしてトランザクション処理を追加します。
bulkInsertとapplyBatchのアトミック性を保証する


●applyBatchのデメリット

・クエリの組み立てが若干複雑
クエリの組み立て方に独特なルールがあります。
また、後方参照を使用しなければならないケースでは可読性が下がります。
といっても、理解すれば問題ないレベルです。

・クエリが途中で失敗した場合の対処
まず、applyBatchがアトミック性を保証していないということ。
さらに、クエリが途中で失敗すると、それ以降のクエリは実行されないということ。
この2点を念頭においてapplyBatchを使用する必要があります。

applyBatchのメリット・デメリットを列挙したところで、本稿ではapplyBatchの概要と、これを使用するためのContentProviderOperationについて記載します。


●概要

applyBatchはクエリ群をContentProviderOperationのリストとして受け取ります。
ContentProviderOperationはinsert/update/deleteクエリを表現するクラスです。
クエリは、ContentProviderOperationに設定した値をもとに生成・発行されます。
ContentResolver.applyBatch  (String, ArrayList<ContentProviderOperation>)
ContentProviderOperation


●ContentProviderOperation.Builder

ContentProviderOperationはContentProviderOperation.Builderクラスから生成します。
ContentProviderOperation.Builder

ContentProviderOperationを構築する簡単なサンプルは下記です。
// Insert文のオペレーションを生成
ContentProviderOperation.newInsert(uri).withValue("name", "test1").build()

// Update文のオペレーションを生成
ContentProviderOperation.newUpdate(uri).withValue("name", "test2").build()

// Delete文のオペレーションを生成
ContentProviderOperation.newDelete(uri).build()
ContentProviderOperation.Builderは用途にあわせてInsert用、Update用、Delete用が用意されています。
ビルダは"どの種類のビルダなのか"を明示してインスタンスを生成します。
  • newInsert(uri) : Insert用ビルダを生成
  • newUpdate(uri) : Update用ビルダを生成
  • newDelete(uri) : Delete用ビルダを生成


●ContentProviderOperationの構築

ContentProviderOperation.Builderには、ContentProviderOperationを構築するためのメソッドが用意されています。
各メソッドはInsert文に特化したものや、Update/Delete文に特化したものがあります。
例えばInsert用ビルダでは、Update/Deleteに特化したwithSelection(...)メソッドは使用できません。
各ビルダ種別毎の使用可能/不可能メソッドの一覧はContentProviderOperation.Builderのjavadocで確認できます。
もし、使用不可能なメソッドを呼び出すと例外が投げられます。
# Insert用ビルダがwithSelectionメソッドを呼び出した場合
java.lang.IllegalArgumentException: 
        only updates, deletes, and asserts can have selections

主なメソッドを下記に列挙します。
InsertやUpdateクエリで追加値・更新値を指定するには下記のメソッドを使用します。
  • ContentProviderOperation.Builder.withValues(ContentValues) 
  • ContentProviderOperation.Builder.withValueBackReference(String, int)
  • ContentProviderOperation.Builder.withValueBackReference(ContentValues)
これらはDelete用ビルダでは使用できないメソッド群です。

***BackReferenceは後方参照です。これについては次回投稿します。
withValuesメソッドを使用した簡単なサンプルは下記です。
// nameにtest1を持つレコードを追加するInsert文
ContentProviderOperation.newInsert(uri).withValue("name", "test1").build();
ContentProviderOperationで表現されるクエリは条件(selection)の指定が可能です。
条件指定には下記のメソッドを使用します。
  • ContentProviderOperation.Builder.withSelection(String, String[]) 
  • ContentProviderOperation.Builder.withSelectionBackReference(int, int) 
これらはInsert用ビルダでは使用できないメソッド群です。

****BackReferenceは後方参照です。これについては次回投稿します。
withSelectionメソッドを使用した簡単なサンプルは下記です。
// _idが3のレコードのnameを"test5"に更新するUpdate文
ContentProviderOperation.newUpdate(uri).withValue("name", "test5")
    .withSelection("_id=?", new String[]{"3"}).build();

●ContentProviderOperationの結果オブジェクト

applyBatchは複数のクエリ(ContentProviderOperation)を実行します。
そのため、複数のクエリ結果をContentProviderResultの配列で返します。

ContentProviderResultはuriとcountを持つシンプルなクラスです。
Insertクエリの結果はuriに、Update/Deleteの結果はcountに格納されます。
ContentProviderResult

applyBatchで実行される各クエリの結果を参照したい場合はContentProviderResultを参照します。


●applyBatchの実行

applyBatchを実行する簡単なコードは下記です。
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Uri uri = Uri
        .parse("content://yuki.contentprovider.mycontentprovider");
operations.add(ContentProviderOperation.newInsert(uri)
        .withValue("name", "test1").build());
operations.add(ContentProviderOperation.newUpdate(uri)
        .withValue("name", "test2").build());
try {
    getContentResolver().applyBatch(
            "yuki.contentprovider.mycontentprovider", operations);
} catch (Exception e) {
    Log.e("yuki", "error");
}
これを実行すると、Insert文とUpdate文がまとめて実行されます。

クエリはArrayList(operations)の先頭から順番に実行されます。
もし途中でクエリの実行が失敗すると、それ以降のクエリは実行されません。
# applyBatchにアトミック性を求めるなら「bulkInsertとapplyBatchのアトミック性を保証する」を参照

以上です。
2012/07/31

Android:bulkInsertとapplyBatchのアトミック性を保証する

●blukInsert

ContentResolverやContentProviderClientにあるbulkInsertはアトミックな操作ではありません。
トランザクションの開始なしに、連続してinsertするAPIです。
ContentResolver.bulkInsert

・android.content.ContentProvider.bulkInsert(Uri, ContentValues[])
/**
 * Override this to handle requests to insert a set of new rows, or the
 * default implementation will iterate over the values and call
 * {@link #insert} on each of them.
 * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()}
 * after inserting.
 * This method can be called from multiple threads, as described in
 * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
 * and Threads</a>.
 *
 * @param uri The content:// URI of the insertion request.
 * @param values An array of sets of column_name/value pairs to add to the database.
 * @return The number of values that were inserted.
 */
public int bulkInsert(Uri uri, ContentValues[] values) {
    int numValues = values.length;
    for (int i = 0; i < numValues; i++) {
        insert(uri, values[i]);
    }
    return numValues;
}

bulkInsertのアトミック性を保証したい場合、独自のプロバイダでbulkInsertをオーバーライドします。
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
    SQLiteDatabase db = MyDataBase.getInstance(getContext())
            .getWritableDatabase();
    db.beginTransaction();
    try {
        SQLiteStatement insertStmt = db
                .compileStatement("INSERT INTO test "
                        + "(name) VALUES (?);");
        for (ContentValues value : values) {
            insertStmt.bindString(1, value.getAsString("name"));
            insertStmt.executeInsert();
        }
        db.setTransactionSuccessful();
    } finally {
        db.endTransaction();
    }
    ...
これでbulkInsertのアトミック性が保証されます。


●applyBatch

blukInsertと同じく。
ContentResolverやContentProviderClientにあるapplyBatchもアトミックな操作ではありません。
まとめてContentProviderOperationを実行するためのAPIで、トランザクションは開始されません。
ContentResolver.applyBatch

・android.content.ContentProvider.applyBatch(ArrayList<ContentProviderOperation>)
/**
 * Override this to handle requests to perform a batch of operations, or the
 * default implementation will iterate over the operations and call
 * {@link ContentProviderOperation#apply} on each of them.
 * If all calls to {@link ContentProviderOperation#apply} succeed
 * then a {@link ContentProviderResult} array with as many
 * elements as there were operations will be returned.  If any of the calls
 * fail, it is up to the implementation how many of the others take effect.
 * This method can be called from multiple threads, as described in
 * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
 * and Threads</a>.
 *
 * @param operations the operations to apply
 * @return the results of the applications
 * @throws OperationApplicationException thrown if any operation fails.
 * @see ContentProviderOperation#apply
 */
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
    final int numOperations = operations.size();
    final ContentProviderResult[] results = new ContentProviderResult[numOperations];
    for (int i = 0; i < numOperations; i++) {
        results[i] = operations.get(i).apply(this, results, i);
    }
    return results;
}

applyBatchのアトミック性を保証したい場合、独自のプロバイダでapplyBatchをオーバーライドします。
@Override
public ContentProviderResult[] applyBatch(
        ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
    SQLiteDatabase db = MyDataBase.getInstance(getContext())
            .getWritableDatabase();
    db.beginTransaction();
    try {
        ContentProviderResult[] result = super.applyBatch(operations);
        db.setTransactionSuccessful();
        return result;
    } finally {
        db.endTransaction();
    }
}
これでapplyBatchのアトミック性が保証されます。

以上です。

2012/07/30

Android:SQLiteのロックとTransaction Immediate/Exclusive


!この記事は古くなっています. 更新版は下記をご覧ください.!
  Android: SQLite3 LockとTransaction Immediate/Exclusive
  http://yuki312.blogspot.jp/2014/10/android-sqlite3-locktransaction.html


SQLiteのロック機構と、Transactionで使用できるロック2種の選択基準についての考察。

●SQLiteのロック

SQLiteのロック単位は"データベース単位"。
このため、ロックを1つ取得すると同データベース上にある全てのテーブルに影響がある。

Oracle等の巨大なDBMSでは"行ロック"なんてものがあったりして細かくロックを制御できるが、軽量なSQLiteは"データベース単位"でのロックのみサポートしている。
そのため、長い時間ロックし続けると他テーブルになかなかアクセスできない状態になる。
全く問題にならないケースもあるが、"データベースを分割するかしないか"の判断基準の1つにはなりそう。


●Transaction Immediate

Transactionで指定できるロック種別の1つがImmediate。
Immediateロックモードのポイントは下記
・ロック中、他ユーザはデータの読み取りはできても書き込みはできない
長所は、ロック中でも他ユーザがデータを読めるため"ロック解除されるまで待たなくて良い"こと。
逆に"読めてしまうことによる弊害"がある場合はExclusiveロックの使用を検討する。

Immediateロックではマズいケースは下記のような場合かな?あまり良い例が思いつかない。
  1.  アプリの設定値を保持したテーブルがある
  2.  アプリの設定値は他モジュールから参照される
  3.  アプリの設定値は可能な限り最新値を返す
  4.  アプリの設定値の更新には多少時間が掛かる
アプリの設定値を更新する必要があるシーン(つまりDBテーブルの値が古い状態)で、設定値の更新を試みる。
ただし、設定値の更新は多少時間がかかる(4)。
Immediateロックでは、更新中(ロック中)にデータベースを参照されると古い値が返されるのでNG(3)。
そのため、参照もできなくするExclusiveロックを掛ける。


●Transaction Exclusive

Transactionで指定できるもう1つのロック種別がExclusive。
Exclusiveロックモードのポイントは下記
・ロック中、他ユーザはデータの読み書きができない
長所は、"ロック中に読み書きできるのは自ユーザのみ"なこと。
逆に"他ユーザの読み取りを待たせてしまうことによる弊害"がある場合はImmediateロックの使用を検討する。

Exclusiveロックだとマズいケースは下記のような場合かな?
  1.  とあるデータAは多数のユーザから頻繁に読み取りアクセスされる
  2.  とあるデータAを読み取るアプリは高い応答性が求められる
  3.  データ更新中に並行してデータ参照されても平気
Exclusiveロックを取得している間、他ユーザは同データベース内にあるテーブルや行/列のデータを参照できない。
そのため、Exclusiveロックが長くなればなるほど、データを参照したいアプリの応答性を損なう原因となり兼ねない(2)。
(3)があてはまるならImmediateロックで問題ないかな?

# 上記2つのケースがImmediate/Exclusiveロックの考え方として正しいのか自信なし。
# もっと良いケースがあれば後日修正します。


●ソースコード

AndroidでのTransactionはSQLiteDataBaseクラスを使用します。

基本的な使い方は割愛。下記サイトで詳しく取り上げられています。
AABlog:Androidアプリ開発 SQLiteデータベースを使用する(トランザクション)

AndroidでTransactionのロック種別を指定するにはSQLiteDataBaseクラスを使用します。
使用するのは下記のAPI
※beginTransactionNonExclusive/beginTransactionWithListenerNonExclusiveはAPI Level11(Honeycomb)で追加されたAPIです。

beginTransaction/beginTransactionWithListenerで開始されたトランザクションは「Exclusiveモード」で、
beginTransactionNonExclusive/beginTransactionWithListenerNonExclusiveで開始されたトランザクションは「Immediateモード」でデータベースをロックします。
// 「Exclusiveモード」でロック
db.beginTransaction();
try {
    // do something.
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

// 「Immediateモード」でロック
db.beginTransactionNonExclusive();
try {
    // do something.
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
簡単ですね。

●SQLiteDatabaseLockedException

ロックされているデータベースに対してSQLiteDatabase経由でクエリを実行した場合、、、

・SQLiteDatabase.query
ロックが取得できるまで待ちます。
API Lv16(JellyBean)以降はCancellationSignalを使ってクエリをキャンセルすることが出来ます。
ContentResolver.query()
CancellationSignal

・SQLiteDatabase.insert
SQLiteDatabaseがSQLiteDatabaseLockedExceptionをキャッチし、呼出し元に-1を返します。
public long insert(String table, String nullColumnHack, ContentValues values) {
    try {
        return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
    } catch (SQLException e) {
        Log.e(TAG, "Error inserting " + values, e);
        return -1;
    }
}
・SQLiteDatabase.insertOrThrow
SQLiteDatabaseLockedExceptionが投げられます。
public long insertOrThrow(String table, String nullColumnHack, ContentValues values)
        throws SQLException {
    return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
}

・SQLiteDatabase.update
SQLiteDatabaseLockedExceptionが投げられます。

・SQLiteDatabase.delete
SQLiteDatabaseLockedExceptionが投げられます。


また、トランザクション開始のbeginTransactionでも同様。

・SQLiteDatabase.beginTransaction
SQLiteDatabaseLockedExceptionが投げられます。


以上です。
2012/07/25

Android:1つのDBに複数のContentProvider


データベースの構造が複雑化すると、これを扱うContentProviderも複雑になりがちです。
そんな時はContentProviderを分割するのも1つの方法です。

今回は複数のContentProviderで1つのデータベースを管理する方法について。

複雑な構造をもつデータベースを、たった1つのプロバイダが管理するのは大変です。
この場合、ContentProviderを分類毎や各機能毎に分割すれば複雑化を回避することができます。
また、ContentProviderの分割はセキュリティの向上にも役立ちます。

簡単な例を示します。

あなたのアプリが持つデータベースの状態が下記の場合...
  1. 1つのデータベースにデータタイプの異なるテーブルが複数存在する
  2. それぞれのテーブルは大きい、小さい、複雑、単純と様々
  3. いくつかのテーブルは外部アプリに提供する情報。いくつかのテーブルは非公開情報

複数のテーブルを管理するだけではなく、それぞれのデータの"公開"or"非公開"を意識する必要があります。
さらには、データの分類も多岐にわたるので、たった1つのContentProviderで管理するには無理がありそうです。


そこで、ContentProviderの分割を検討します。

まず、1つのContentProviderは1つのデータ分類を管理するようにします。
これでContentProviderの凝集度も上がります。

次に、公開or非公開のデータをどのように扱うかを考えます。
今回の場合は"データ分類B"が非公開情報です。
これは、データ分類BのContentProviderに対するアクセス権限を設定することで解決します。

対応後のイメージは下記になります。


それぞれのContentProviderを定義するマニフェストは次のようになります。
# あくまでサンプルです。実際には適切な権限と名前を割り当てます
...
<provider
    android:name="yuki.divcontentprovider.GlobalContentProvider"
    android:authorities="yuki.divcontentprovider.global" />
<provider
    android:name="yuki.divcontentprovider.LocalContentProvider"
    android:authorities="yuki.divcontentprovider.local"
    android:readPermission="yuki.divcontentprovider.dangerous" >
    <path-permission
        android:path="/ok"
        android:readPermission="yuki.divcontentprovider.normal" />
</provider>
...
GlobalContentProviderは公開情報(つまりデータ分類A)用のContentProviderです。
LocalContentProviderは非公開情報(つまりデータ分類B)用のContentProviderです。

LocalContentProviderはパスによって要求するパーミッションを変えています。
ContentProviderにはパスに対するパーミッション付与の機能が備わっているので制御も簡単です。

ソースコードに目を向けてみます。
ContentProviderを分割すると問題なのがデータベースの扱いです。
データベースの生成やアップグレードを担うSQLiteOpenHelperの設計には少し注意が必要です。

複数のContentProviderがいても、データベースは1つなのでSQLiteOpenHelperを継承するクラスはSingletonにします。
public class AppDataBase extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "hoge";
    private static final int DATABASE_VERSION = 1;

    private static AppDataBase sSingleton = null;

    public interface Table {
        public static final String TABLE1 = "TABLE_NAME";
    }

    public static synchronized AppDataBase getInstance(Context context) {
        if (sSingleton == null) {
            sSingleton = new AppDataBase(context);
        }
        return sSingleton;
    }

    public AppDataBase(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        ...
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        ...
    }
}
分割されたContentProvider達はAppDataBase.getInstance()メソッドを使用してインスタンスを取得します。

以上です。
2012/07/20

Android:抽象ドット密度tvdpi

抽象的なドット密度を表すリソース修飾子 tvdpiAndroid3.2で追加されました。
Android 3.2 Platform

tvdpiはテレビやそれに類似したデバイス向けに用意されています。
最近ではNexus7がtvdpiを持つ端末として知られています。
Getting Your App Ready for Jelly Bean and Nexus 7


●tvdpi概要

tvdpiは213dpi、mdpiより1.3312501倍のドット密度を持ちます。
これにより、各抽象ドット密度比は
 120:160:213:240:320 = 3 : 4 : 5.325 : 6 : 8
になります。

mdpiで縦横100pxの画像を用意する場合、tvdpiでは縦横133pxの画像が必要です。


●tvdpiを試す

下記の設定値をもつエミュレータでのAVDを作成することでtvdpiの動作を確認できます。
  • Skin Resolution:1280x800  or 600x960(Nexus7)
  • Abstract LCD:213
手元の環境だとSkinをWXGAとしても213dpiになりませんでした。
なので、手打ち(Resolution)で解像度を指定します。


●tvdpiのリソース修飾子選択基準

xxx-hdpi と xxx-tvdpi それぞれのリソースを持つアプリを下記の環境で動作させた場合どうなるのかを検証します。

・240dpiの端末で実行
結果:hdpiが参照される。もしhdpiリソースを持っていない場合はtvdpiが参照される。


・213dpiの端末で実行
結果:tvdpiが参照される。もしtvdpiリソースを持っていない場合はhdpiが参照される。

後者については、よりhigh densityなhdpiがダウンスケールされて参照されます。
don’t panic! We actively discourage you from rushing out and creating new assets at this density; Android will scale your existing assets for you. In fact the entire Jelly Bean OS contains only a single tvdpi asset, the remainder are scaled down from hdpi assets.    
[from Getting Your App Ready for Jelly Bean and Nexus 7]


以上です。

2012/07/19

Android:ACTION_MANAGE_NETWORK_USAGEのサポート


●Android4.0で追加されたNetwork Usage機能


Android4.0で、アプリのネットワーク使用量をユーザが確認できるNetwork Usage機能が追加されました。
アプリ毎やネットワーク種別毎に通信量の上限を設定することも可能です。

これはつまり、ユーザはネットワーク通信量や頻度が確認可能であり、またネットワーク通信量に関して興味を持つことに繋がります。
そういった意味で、ネットワーク通信を必要とするアプリにとって、その通信量・頻度が確認可能になったことによる影響を考える必要が出てきます。
Android developer -Android 4.0 for Users / Control over network data-



●インテントアクション:ACTION_MANAGE_NETWORK_USAGE


Network Usageでは、各アプリ毎の通信量や個別の設定を行うことが可能です。
アプリ毎の詳細画面では下記が確認・設定できます。
  • アプリのフォアグラウンド時の通信量
  • アプリのバックグラウンド時の通信量
  • アプリ個別のネットワーク設定の起動
  • バックグラウンドデータ通信の制限
ユーザはモバイル回線の通信量に上限を設けることができます。
また、アプリ毎にモバイル回線使用時のバックグラウンド通信を制限できます。
しかし、アプリによってはこれだけの設定項目では不十分な場合があります。
例えば、メールアプリでは新着メールのチェック機能を制限したくない場合です。
こういったアプリの特性を考慮した、より細かい通信制御は各アプリに委ねられます。

アプリ毎の詳細画面には[View app settings]ボタンが用意されています。
これは、そのアプリが用意したネットワーク設定を起動するトリガになるボタンです。
ネットワーク設定をサポートするには、アプリのネットワーク設定ActivityのIntentFilter
に下記を追加します。
<intent-filter>
    <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
[View app settings]ボタンが押されると、MANAGE_NETWORK_USAGEアクションでアプリの
ネットワーク設定画面を起動します。
IntentFilterが登録されていないと[View app settings]ボタンは無効化されます。

用意したネットワーク設定画面で、ユーザが通信を細かく制御できる仕組みを提供することが推奨されます。

以上です。
2012/07/18

Android:ブロードキャストを受信するコンポーネントを調べる方法

特定のBroadcastIntentを受信するBroadcastReceiverの一覧を取得するには次のコマンド
を使用します。
$ adb shell dumpsys activity broadcasts
実行すると登録されているIntentFilterの一覧が取得できます。
ACTIVITY MANAGER BROADCAST STATE (dumpsys activity broadcasts)
  Registered Receivers:
  * ReceiverList{421bc0e0 8893 com.example.networkcheck/10146 remote:4227fdf0}
    app=ProcessRecord{41c1f560 8893:com.example.networkcheck/10146} pid=8893 uid=10146
    Filter #0: BroadcastFilter{421bc140}
      Action: "android.net.conn.CONNECTIVITY_CHANGE"
  ...略
この例だと、com.example.networkcheckコンポーネントが
Action名:android.net.conn.CONNECTIVITY_CHANGE のインテントフィルタを登録してい
ることがわかります。

以上です。
2012/07/17

Android:エイリアスを使ったマルチスクリーン対応


画面サイズによって、レイアウトのパネル数を変化させるテクニックは有名です。
レイアウトパターンの1つMaster/Detailパターンは、タブレット等の大画面端末でよく
使われるパターンですが、ハンドセット端末では画面領域が限られている為、ほとんど使
われません。

今回は、もしあなたのアプリに下記の要求があった場合、
  • 画面サイズによってパネル数を変化させる必要がある
  • Android3.0との互換性も考える必要がある
どのようにレイアウトリソースを定義すればよいかを考えます。
(Android3.0より前のバージョンでも使えるテクニックです)

●リソース別名を使わない場合

まずは、画面領域が小さい端末(ハンドセット等)を対象にしたレイアウト。
これは1パネルレイアウトになります。
・res/layout/main.xml
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:name="yuki.sample.ItemListFragment"
    android:id="@+id/item_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp" />
次に、画面領域が大きい端末(タブレット等)を対象にしたレイアウト。
これは2パネルレイアウトになります。
・res/layout-sw600dp/main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:divider="?android:attr/dividerHorizontal"
    android:showDividers="middle">

    <fragment android:name="yuki.sample.ItemListFragment"
        android:id="@+id/item_list"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <FrameLayout android:id="@+id/item_detail_container"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3" />

</LinearLayout>
さらに、リソース修飾子sw<N>dpはAndroid3.2以降に追加されたため、
Android3.1以前をサポートするために抽象画面サイズのlarge版を用意します。
・res/layout-large/main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    .... />
    // 内容はres/layout-sw600dp/main.xmlと同じ
</LinearLayout>
今回用意したレイアウトファイルは次の3つです。
  • res/layout/main.xml (1パネル)
  • res/layout-sw600dp/main.xml (2パネル)
  • res/layout-large/main.xml (2パネル)

●リソース別名を使った場合

もし、sw600dpとlargeで用意したレイアウトファイルのような、レイアウト定義の重複を
避けたい場合、レイアウト別名ファイルを用意します。

例えば、次のような2ファイルを用意します。
  • res/layout/one_panels.xml (1パネル)
  • res/layout/two_panels.xml (2パネル)

そして、レイアウト別名を定義するファイルを用意します。
・res/values/layout.xml:
<resources>
    <item name="main" type="layout">@layout/one_panels</item>
</resources>
・res/values-sw600dp/layout.xml:
<resources>
    <item name="main" type="layout">@layout/two_panels</item>
</resources>
・res/values-large/layout.xml:
<resources>
    <item name="main" type="layout">@layout/two_panels</item>
</resources>
それぞれのファイルではレイアウト自体を定義していません。
レイアウトの別名を定義することでレイアウト定義の重複を避けています。
sw600dpの端末でレイアウト"main"を参照すると、結果的にres/layout/two_panels.xml
を参照することになります。
レイアウトを読み込むjavaコード側では、mainレイアウトリソースを読み込むだけで、
画面サイズにあった適切なレイアウトが取得できます。

画面サイズのみならず画面の向き(land or port)でもレイアウトを変化させるといった、
レイアウトリソースが爆発的に増えやすい仕様のアプリで有効な方法です。

●現在のレイアウトは1パネルか?2パネルか?

javaコード側で、現在のレイアウトが1パネルか2パネルかを判断する方法について考えます。
まず思い浮かぶのは下記でしょう。
if (findViewById(R.id.item_detail_container) != null) {
    // 2パネルモード
} else {
    // 1パネルモード
}
2パネルレイアウトでのみ定義しているitem_detail_containerが見つかれば2パネルモー
ドであると判断します。

しかし、この方法だとjavaコード側がレイアウトの詳細を知っていることになります。
さらに、レイアウト定義側もitem_detail_containerの扱いに注意が必要です。
この厄介な問題を解決するには、2パネルかどうかの判断フラグをリソースとして定義す
る方法があります。
・res/values/layout.xml:
<resources>
    <item name="main" type="layout">@layout/one_panels</item>
    <bool name="has_multi_panels">false</bool>
</resources>
・res/values-sw600dp/layout.xml:
<resources>
    <item name="main" type="layout">@layout/two_panels</item>
    <bool name="has_multi_panels">true</bool>
</resources>
javaコード側ではhas_multi_panelsのboolリソースを参照してパネル数を判断します。
こうすることで、javaコードとレイアウトリソースそれぞれの結合度を低く保つことが
できます。

より詳しい情報は下記を参照。
http://developer.android.com/training/multiscreen/screensizes.html

以上です。
2012/07/14

Android:ic_launcher-web.png


ADT Rev.20でプロジェクトを新規作成するとic_launcher-web.pngという名の画像ファイル
が、プロジェクトルート直下に自動生成されています。

これはGoogle Playにアプリを公開する時に必要となる
 "高解像度アプリケーション アイコン"
です。

デベロッパー向けGoogle Play - アプリケーション用の画像アセット

この画像は512x512pxサイズのPNG(αチャネル付)で作成する必要があります。
主にPC版Google Playのアプリアイコンとして使用されます。

以上です。
2012/07/12

Android:ADT Rev.20 雛形MasterDetailFlow


図1


ADT Rev.20でAndroidプロジェクトを新規作成する際の"Create Activity"(図1)で
"MasterDetailFlow"を選択した場合に作成される雛形の設計・実装を調査しました。

MasterDetailFlowはよくある2パネルのアプリを作成します。
ただし、画面領域を十分に確保できない場合は1ペインで表示します。
イメージはこんな感じです。
http://developer.android.com/images/fundamentals/fragments.png

MasterDetailFlowなプロジェクトを作成すると下記の構成で雛形が作成されます。


パッと見でこの辺がパネル数を制御してそうですね。
  • layout/activity_item_twopane.xml
  • values-large/refs.xml
  • values-sw600dp/refs.xml

activity_item_twopane.xmlは2パネル用のレイアウトファイル。
<LinearLayout
    ...一部省略...
    android:orientation="horizontal"
    android:showDividers="middle">

    <fragment android:name="com.example.fragment.ItemListFragment"
        ...一部省略...
        android:layout_weight="1" />

    <FrameLayout android:id="@+id/item_detail_container"
        ...一部省略...
        android:layout_weight="3" />

</LinearLayout>
リストと詳細の画面割当は1:3。それぞれの間はdividerで区切ってます。
リストパネルはItemListFragmentで管理。
詳細パネルはitem_detail_containerのIDを持つ空コンテナで、ItemDetailFragment
ではないようです。

次に2パネルレイアウトのactivity_item_twopane.xmlを読み込んでいる人を探します。
順当にレイアウト参照元を辿っていくと...

はじめに目をつけた通り、↓のようです。
  • values-large/refs.xml
  • values-sw600dp/refs.xml

どちらも内容は全く同じ。
layoutリソースactivity_item_twopaneをactivity_item_listとして定義してる。
<resources>
    <item type="layout" name="activity_item_list">@layout/activity_item_twopane</item>
</resources>
と、ここでもう一度layoutフォルダの中を見るとactivity_item_list.xmlが既に存在し
ている。
そのため、先ほどのrefs.xmlはlayoutリソースの再定義(上書き)となる。

まとめると、refs.xmlによって下記のようにリソースが定義されている。
  • デフォルト=activity_item_listはそのままactivity_item_list
  • 大画面端末=activity_item_listはactivity_item_twopane.xmlで上書き
  • 画面最短幅が600dp以上=activity_item_listはactivity_item_twopane.xmlで上書き
ということで、レイアウトactivity_item_listを読み込めば適切なリソースが選択されます。
refs.xmlでのリソース再定義は結構スマートですね。


activity_item_listを読み込んでいるのはItemListActivity。
単にactivity_item_listを読み込むだけで、1パネルor2パネルのレイアウトが適切に
選択されます。
Activity自身はonCreate内で詳細表示領域の空コンテナ(item_detail_container)がレイ
アウト内に存在しているかをチェックして、2パネルモードかどうかを判定しています。

また、ActivityがFragmentの状態を管理(ItemListFragment.setActivateOnItemClick)
していることもわかります。
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_item_list);

    if (findViewById(R.id.item_detail_container) != null) {
        mTwoPane = true;
        ((ItemListFragment) getSupportFragmentManager()
                .findFragmentById(R.id.item_list))
                .setActivateOnItemClick(true);
    }
}

ItemListActivityはonItemSelectedメソッドをオーバーライドしています。
これはItemListFragment.Callbacksインタフェースで定義されたコールバックメソッド。
ItemListFragmentでリストが選択されるとここにコールバックされます。
Fragment⇒Activityの連携はコールバックで実現しているようです。
この辺りはDeveloperサイトでも紹介されている方法ですね。
http://developer.android.com/training/basics/fragments/communicating.html
public class ItemListActivity extends FragmentActivity
        implements ItemListFragment.Callbacks {

    //...省略...

    @Override
    public void onItemSelected(String id) {
        if (mTwoPane) {
            Bundle arguments = new Bundle();
            arguments.putString(ItemDetailFragment.ARG_ITEM_ID, id);
            ItemDetailFragment fragment = new ItemDetailFragment();
            fragment.setArguments(arguments);
            getSupportFragmentManager().beginTransaction()
                    .replace(R.id.item_detail_container, fragment)
                    .commit();
        } else {
            Intent detailIntent = new Intent(this, ItemDetailActivity.class);
            detailIntent.putExtra(ItemDetailFragment.ARG_ITEM_ID, id);
            startActivity(detailIntent);
        }
    }
}

詳細画面の表示に関して。
2パネルの場合、空コンテナitem_detail_containerをFragmentのreplaceトランザクショ
ンで置き換えるようにしています。
1パネルの場合は、Activity自身でstartActivityを実行しているのがわかります。

詳細画面への情報連携(どのリストアイテムを選んだか)はIntentのextra値を使用。
リストアイテム選択時の処理はActivityまかせですね。


コールバック部分をもう少し追ってみます。

ItemListFragmentはコールバックリスナーであるmCallbacksを管理します。
初期値はsDummyCallbacks。こいつはNullオブジェクトの役目をします。
private Callbacks mCallbacks = sDummyCallbacks;

public interface Callbacks {
    public void onItemSelected(String id);
}

private static Callbacks sDummyCallbacks = new Callbacks() {
    @Override
    public void onItemSelected(String id) {
    }
};

mCallbacksはonAttachで登録。onDetachでNullオブジェクト化されます。
登録時はactivityインスタンスをCallbacksにキャストするため、事前にキャスト可能か
チェックをしてます。
このことから、ItemListFragmentを使うActivityはCallbacksインタフェースを実装する
必要があります。

Developerサイトではtry-catchでcallback登録していましたが、instanceOfで例外判定す
るこちらのほうが良い方法といえそうですね。
http://developer.android.com/training/basics/fragments/communicating.html#DefineInterface
@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    if (!(activity instanceof Callbacks)) {
        throw new IllegalStateException("Activity must implement fragment's callbacks.");
    }

    mCallbacks = (Callbacks) activity;
}

@Override
public void onDetach() {
    super.onDetach();
    mCallbacks = sDummyCallbacks;
}

@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
    super.onListItemClick(listView, view, position, id);
    mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id);
}

●おわりに...

MasterDetailFlowの雛形からは、Fragment⇒Activityへのメッセージ通信方法と、
画面サイズに依存したパネル数の変更方法についてのヒントがありました。

もし、手元にスマートフォンしかない場合、この雛形では縦横切り替えしてパネル数が
変化する動作を確認できません(sw600dp or largeを満たさない為)
values-sw600dp ⇒ values-land に変更すればスマートフォンでも手軽にこれを確認
できます。

以上です。