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ディレイで送信していることが確認できます。

以上です。