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はユーザを強く惹きつけますね。
品質に大きく関わる部分でもありますのでこの辺は注意したいところです。

以上です。