2016年8月31日水曜日

マルチユーザ対応 ANDROID 4.2以降の内部ストレージと外部ストレージ

マルチユーザ対応に伴い、Android 4.2からストレージの扱いが変わりました。
Android 4.2(JellyBean)
からマルチアカウント機能が追加され、複数人でAndroid端末を共有できるようになりました。この変更に伴い、ユーザごと、異なったアプリケーションデータの保存領域が用意されています。適切なディレクトリ(Path)を取得する方法を紹介します。

マルチユーザ対応でアプリデータの保存場所が変わる

マルチアカウント機能はタブレットでのみ利用できる機能ですが、Phoneデバイスも(4.2以降のバージョンでは)これら変化点の影響を受けています。
・アプリケーションデータの保存領域
・内蔵ストレージ領域
2つの場所がユーザーごとに変化してしまいます。そのためアプリ内で直接参照や指定している場合は想定の場所にファイルが見つからずアクセスに失敗することになります。

 

まとめ

マルチユーザ対応の有無にかかわらず以下のAPIを使うことで正しい保存先ディレクトリが取得できます

§  アプリケーションデータの保存領域にアクセスする場合

§  ActivityクラスのgetFilesDirメソッド(/data/xxx/org.techbooster.sample.multiuser/files)を使う。

§  アプリケーションデータの保存領域にあるファイルを開く場合

§  ActivityクラスのopenFileOutput("data.dat",MODE_PRIVATE)を使う。
このメソッドはgetFilesDir()ディレクトリ以下の同名ファイルを開きます(なければ新規作成されます)

§  内部ストレージにアクセスする場合

§  EnvironmentクラスのgetExternalStorageDirectoryメソッド(内部ストレージのトップディレクトリを開く)

§  EnvironmentクラスのgetExternalFilesDirメソッド(内部ストレージ内アプリ保存ディレクトリを取得)

を活用してください。

詳細は続きから。内蔵ストレージのPathなどはAndroid端末ごとに異なるため(メーカーごとに傾向はあるものの)Nexus7をベースに説明します。


Nexus7
のファイルシステム(フォルダ)構成

Nexus7では図のようにアプリケーションデータは/data/dataに、内部ストレージは/storageにマウントされています。
内部ストレージは機種依存があり、/mnt/sdcard//sdcardの場合もあります。
外部ストレージは更に依存が大きく、/mnt/external_sdや存在しない場合もあります。
このような状況下ではアプリ内でPathをハードコーディングして覚えておくのはリスクが高く互換性を低下させます。

アプリケーション専用の保存場所を取得するには

アプリケーションごとの保存領域はActivityで用意されている(Contextクラスの抽象)メソッドを使って簡単に取得できます

メソッド名

説明

getFilesDir()

ファイルを置くディレクトリPathを取得する

getCacheDir()

キャッシュ用のPathを取得する

openFileOutput(String, int)

getFilesDirで取得できるPath以下にファイルを作成する。第1引数にファイル名、第2引数にアクセスモードを指定(MODE_PRIVATEが一般的)する。


ActivityクラスのgetFilesDirメソッド、getCacheDirメソッドを使うと端末で指定されているアプリケーション専用のデータ領域を取得できます。
ログにパスを出力するだけの簡単なサンプルコードを用意しました。実行してマルチユーザ時の挙動を確認してみましょう。

1

2

3

4

5

//アプリケーション専用データ領域

//ファイル保存ディレクトリ

Log.d("Multi", " getFilesDir(): " + getFilesDir());

//キャッシュ保存ディレクトリ(消去される可能性あり)

Log.d("Multi", " getCacheDir(): " + getCacheDir());

ちなみにCacheDir1MB程度が推奨されており、フラッシュメモリ容量が少なくなってくるとシステムにより自動的に削除されます。キャッシュの場合、なくなっても再取得すればよいため、問題ありません。逆になくなっては困る用途には利用しないでください。またデータの自動削除が行われるものの、本体のフラッシュメモリ残量に依存するため、アプリ独自で容量管理したほうが良いでしょう。

シングルユーザ時

シングルユーザで動作している場合、得られる絶対パスはいままで通りです。
■UserA

1

2

D/Multi(3731):  getFilesDir(): /data/data/org.techbooster.sample.multiuser/files

D/Multi(3731):  getCacheDir(): /data/data/org.techbooster.sample.multiuser/cache

アプリケーションディレクトリ以下のfilescacheディレクトリの絶対パスが取得できます。

マルチユーザ時

マルチユーザの例としてUserAUserBを作成した場合、図を基準としてgetFilesDirメソッドとgetCacheDirメソッドで取得できる絶対パスが変わります

A
Bを切り替えてアプリケーションをインストールした結果、以下のログが出力されます。
■UserA

1

2

D/Multi(7708):  getFilesDir(): /data/data/org.techbooster.sample.multiuser/files

D/Multi(7708):  getCacheDir(): /data/data/org.techbooster.sample.multiuser/cache

■UserB

1

2

D/Multi(6758):  getFilesDir(): /data/user/10/org.techbooster.sample.multiuser/files

D/Multi(6758):  getCacheDir(): /data/user/10/org.techbooster.sample.multiuser/cache

UserB/data/user/10/以下に独自のアプリケーションディレクトリを持つことになります。
/data/data/を基点として各ユーザが相互に参照出来ない場所に展開されます。LinuxWindowsHomeディレクトリに近い挙動ですね。

内部ストレージのディレクトリを取得する

内部ストレージもユーザごとにエミュレート(仮想デバイスとして認識)しています。Nexus7の場合、それぞれユーザごとに/storage/emulated/id/で内部ストレージが用意されています。

内部ストレージのデータはユーザごとに独立しているため、相互に参照できません。また利用にはパーミッションが要求されます(Android 4.1よりR/W権限ともに用意されるようになりました、またWRITE_EXTERNAL_STORAGEパーミッションはREADも可能です)。ストレージを利用する際はマニフェストに権限を追記します。
■AndroidManifest.xml

1

2

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

SDカード(外部ストレージ)の扱いはどうなる?

すこし本編から外れますが、内部ストレージと同様に気になる外部ストレージの現状をまとめておきます。
Android SDK
の仕様からはSDカードの利用を便利にするAPIは見つかりませんでした。実のところ現在あるAPIの表記もExternalStorageで統一されており内部・外部の区別はありません(区別なく扱えるように内部/外部を判定するメソッドがあります)。過去、フラッシュメモリ容量が少ない時代はExternalStorage(拡張ストレージ)としてSDカードが活用されていましたが内蔵メモリが増えるに従ってSDカードが不要となり、現在のようなアプリケーション領域・内部ストレージ・外部ストレージの3つが入り組んだ構成に至っています。

ストレージの基本思想は今現在で以下のとおりです(mhidakaまとめ)

ストレージの基本思想

アプリケーション領域:基本的にユーザーには見せず、アプリケーションの動作に必要なファイルを置く。
内部ストレージ:PC接続時はUSBデバイス(MTP)として表示。音楽や動画、写真、データを置く。
外部ストレージ:用途、思想、アクセス権などは内部ストレージに準拠する。カードを抜いて読み取るのが簡単。

Android 2.3系まではPCとの接続にUSB MASS Storageを利用していました。Android 4.0以降、USB MASS Storageでの接続は廃止され、MTP(Media Transfer Protocol)に移行しています。これは今後の展開を見据えて変更したと考えられます。実際、UserAPCに接続すると/storage/emulated/0の中身が見え、UserBであれば/storage/emulated/10の中身が表示されます。

いまも拡張性を重視するメーカーは独自に外部ストレージ(SDカード)をサポートしていますが、
マウントポイント(SDカードにアクセスすための絶対パス)

§  /sdcard

§  /mnt/sdcard

§  /mnt/sdcard/external_sd

§  /storage/sdcard0

など安定しておらず、読み書きする方法も機種依存です。マルチユーザ対応とは無関係に今後もこのような状況が続くでしょう(リムーバブルデバイスであり誰でも読み取れるのが特徴ですが、マルチユーザ時のアクセス制御は難しくなります)。

閑話休題

内部ストレージを利用するには

内部ストレージはユーザごと用意されています。アプリケーションデータと同様に共用できません。ゲームデータなど大きなサイズをダウンロードする場合、ユーザごと複数ダウンロードしてしまって内部ストレージ容量を圧迫することになります。内部ストレージは写真などプライベートなデータが格納されることも多く共用できないのもユーザから見ると利点の一つです(セキュリティの観点から)。

メソッド名

説明

getExternalStorageDirectory()

内部ストレージの絶対パスを取得する

isExternalStorageEmulated()

マルチユーザ対応した仮想ストレージを判定する

isExternalStorageRemovable()

SDカードのような取り外し可能なストレージか判定する

getExternalStoragePublicDirectory()

内部ストレージの共有ディレクトリ(音楽や動画などアプリ間で共用するファイルを置く)

getExternalFilesDir(String)

アプリごと異なる。内部ストレージの保存ディレクトリを取得。なければ作成する

Environmentクラスからパスを取得して、ログに表示するサンプルコードを用意しました。
アプリのデータを内部ストレージに置く場合はgetExternalFilesDirメソッドが便利です。package名を含んだディレクトリを作成するため他のアプリと重複しません(内部ストレージへの書き込み権限をもった他のアプリがデータを改変、消去する可能性は残る)
isExternalStorageEmulatedメソッドを使えばエミュレートされた仮想ストレージか判定できるほか、isExternalStorageRemovableメソッドでリムーバブル(取り外し可能)か確認できます。

■/src/MultiUesrActivity.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

 

    //内部ストレージ

    //仮想デバイス確認

    Log.d("Multi", " isExternalStorageEmulated: "

            + Environment.isExternalStorageEmulated());

    //リムーバブルデバイス確認

    Log.d("Multi", " isExternalStorageRemovable: "

            + Environment.isExternalStorageRemovable());

    //内部ストレージのPath取得

    Log.d("Multi"," getExternalStorageDirectory(): "

                    + Environment.getExternalStorageDirectory());

    //内部ストレージの共有ディレクトリ取得

    Log.d("Multi",

        " getExternalStoragePublicDirectory(DIRECTORY_MUSIC): "

        + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC));

 

    //内部ストレージのアプリ情報の保存

    //キャッシュ保存ディレクトリ

    Log.d("Multi", " getExternalCacheDir(): " + getExternalCacheDir());

    //アプリケーションデータ保存ディレクトリ

    Log.d("Multi", " getExternalFilesDir(null): " + getExternalFilesDir(null));

    Log.d("Multi", " getExternalFilesDir(mydata): "

            + getExternalFilesDir("mydata"));

 

}

シングルユーザ利用時

Nexus7のユーザが1名しか登録がない場合のログです。
■UserA

1

2

3

4

5

6

7

8

9

10

11

D/Multi(3731):  isExternalStorageEmulated: true

D/Multi(3731):  isExternalStorageRemovable: false

D/Multi(3731):  getExternalStorageDirectory(): /storage/emulated/0

D/Multi(3731):  getExternalStoragePublicDirectory(DIRECTORY_MUSIC):

/storage/emulated/0/Music

D/Multi(3731):  getExternalCacheDir():

  /storage/emulated/0/Android/data/org.techbooster.sample.multiuser/cache

D/Multi(3731):  getExternalFilesDir(null):

  /storage/emulated/0/Android/data/org.techbooster.sample.multiuser/files

D/Multi(3731):  getExternalFilesDir(mydata):

  /storage/emulated/0/Android/data/org.techbooster.sample.multiuser/files/mydata

全ての絶対パスに/storage/emulated/0が含まれており、エミュレートされた内蔵ストレージが割りあたっています。マルチユーザを意識しないアプリでも内部ストレージのパスはEnvironmentクラスより取得しないと想定外の動作になることがわかります。

マルチユーザ利用時

Nexus7UserAUserB2つのアカウントを登録した状態で、それぞれログを出力しました。
取得できた内部ストレージのパスはUserA,Bで異なることが確認できます。getExternalStoragePublicDirectoryメソッドを利用した場合でも、/storage/emulated/0/Musicが取得できるため、メソッドにPublicとついてもユーザごと個別のストレージが返却されます。
■UserA

1

2

3

4

5

6

7

8

9

10

11

D/Multi(7708):  isExternalStorageEmulated: true

D/Multi(7708):  isExternalStorageRemovable: false

D/Multi(7708):  getExternalStorageDirectory(): /storage/emulated/0

D/Multi(7708):  getExternalStoragePublicDirectory(DIRECTORY_MUSIC):

  /storage/emulated/0/Music

D/Multi(7708):  getExternalCacheDir():

  /storage/emulated/0/Android/data/org.techbooster.sample.multiuser/cache

D/Multi(7708):  getExternalFilesDir(null):

  /storage/emulated/0/Android/data/org.techbooster.sample.multiuser/files

D/Multi(7708):  getExternalFilesDir(mydata):

  /storage/emulated/0/Android/data/org.techbooster.sample.multiuser/files/mydata

UserAの内部ストレージは/storage/emulated/0に割りあたっています。

■UserB

1

2

3

4

5

6

7

8

9

10

11

D/Multi(6758):  isExternalStorageEmulated: true

D/Multi(6758):  isExternalStorageRemovable: false

D/Multi(6758):  getExternalStorageDirectory(): /storage/emulated/10

D/Multi(6758):  getExternalStoragePublicDirectory(DIRECTORY_MUSIC):

  /storage/emulated/10/Music

D/Multi(6758):  getExternalCacheDir():

  /storage/emulated/10/Android/data/org.techbooster.sample.multiuser/cache

D/Multi(6758):  getExternalFilesDir(null):

  /storage/emulated/10/Android/data/org.techbooster.sample.multiuser/files

D/Multi(6758):  getExternalFilesDir(mydata):

  /storage/emulated/10/Android/data/org.techbooster.sample.multiuser/files/mydata

UserBの内部ストレージは/storage/emulated/10に割りあたっています。
原則、ユーザーが増えるごとに/storage/emulated/20,30と増えていきますが数字にユーザ識別用以上の意味はありません。
この絶対パスをハードコーディングしては元の木阿弥ですので内部ストレージのPathを知りたい場合、EnvironmentクラスのgetExternalStorageDirectoryメソッド(内部ストレージのトップディレクトリ)getExternalFilesDirメソッド(内部ストレージ内アプリディレクトリ)を活用してください。

拡張ストレージのパーミッションポリシー変更

Android 4.4からはアプリケーション固有の領域にアクセスする場合において、WRITE_EXTERNAL_STORAGEREAD_EXTERNAL_STORAGEパーミッションを必要としなくなりました。具体的にはgetExternalFilesDir()メソッドで取得できるディレクトリ以下はパーミッションが不要です(今まで前述のパーミッションが必要でした)。

getExternalStoragePublicDirectory()メソッドで取得できるディレクトリはアプリケーション固有ではなくすべてのアプリケーションから共用する領域です。このような領域は外部ストレージとみなされます(の可能性がある)ので今まで通りWRITE_EXTERNAL_STORAGEREAD_EXTERNAL_STORAGEパーミッションが必要です。

この変更はAndroid 4.4から適用されるため、すぐに影響があるわけではありません。バージョン4.4以上の端末が一定以上普及したら切り替えられる可能性が高いため、このような傾向にあると覚えておくとよいでしょう。

関連する記事:

§  SDカードにファイルを保存する

§  Android 4.4で追加されたパーミッションとDevice Features

§  カメラプレビューをキャプチャする

§  Android 4.2 JellyBeanがリリースされました

§  ActivityGroupをつかって複数Activityを表示する