assetsディレクトリのストレージ展開

リソースとして管理したくないデータ(常時メモリ展開したくないデータ)をassetsにディレクトリ構成で配置して、起動時にストレージに丸ごとディレクトリコピーを実装してみましたが、AssetManagerの制約、ファイルサイズ制限等で色々と難儀したのでメモします。

AssetManagerのディレクトリ判定

assetsフォルダ配置内は静的に配置を目的としているためか、抽象表現であるFileが扱えない仕様となっています。問題としてはlist()で取得したパスのディレクトリ・ファイルを判別する手段がありません。

AssetManagerの主要メソッド
String[] list(String path) 引数のパスに含まれる全ての子のパスを取得する
InputStream open(String filename, int accessMode) 引数のパスのInputStreamを取得する
AssetFileDescriptor openFD(String filename) 引数のパスのFileDescriptorを取得する
XmlResourceParser openXmlResourceParser(String filename) 引数のパスのXmlParserを取得する


代替案として、パスをlist()で取得可能またはそのパスをopen不可だった場合はディレクトリと判断するようにします。(※空ディレクトリのパスの場合、open()にてFileNotFoundExceptionが発生します)

     private boolean isDirectory(final String path) {
          boolean isDirectory = false;
          try {
               if (assetManager.list(path).length > 0){ //子が含まれる場合はディレクトリ
                    isDirectory = true;
               } else {
                    // オープン可能かチェック
                    assetManager.open(path);
               }
          } catch (FileNotFoundException fnfe) {
               isDirectory = true;
          }
          return isDirectory;
     }

assetsからStorageへのディレクトリコピー

ディレクトリの全コピーなのでcopyのロジックは再帰メソッドで実装しますが、AssetManagerは
Fileオブジェクトが扱えないのでassetsのpathで扱うことになります。

     private void copyFiles(final String parentPath, final String filename, final File toDir) {

          String assetpath = (parentPath != null ? parentPath + File.separator + filename : filename);
          if (isDirectory(assetpath)) { //ディレクトリ判定
               if (!toDir.exists()) {
                    //出力先のディレクトリ作成
                    toDir.mkdirs();
               }
               for (String child : assetManager.list(assetpath)) {
                    //再帰呼出
                    copyFiles(assetpath, child, new File(toDir, child));
               }
          } else {
               //バイナリコピー
               copyData(assetManager.open(assetpath), new FileOutputStream(new File(toDir.getParentFile(), filename)));
          }
     }

非圧縮ファイルサイズ制限

Android OSではUNCOMPRESS_DATA_MAX(約1MB)で指定された以上のファイルが扱えない仕様となっています。
http://pentan.info/android/app/assets_data_max.html
1MBを超える可能性のあるファイルについては圧縮してassetに配置する必要があります。
zip解凍にはZipInputStreamを利用して展開します。

     private void unzip(InputStream is, File toDir) {
          ZipInputStream zis = null;
          try {
               zis = new ZipInputStream(is);
               ZipEntry entry;
               while ((entry = zis.getNextEntry()) != null) {
                    String entryFilePath = entry.getName().replace('\\', File.separatorChar);
                    File outFile = new File(toDir, entryFilePath);
                    if (entry.isDirectory()) {
                         outFile.mkdirs();
                    } else {
                         BufferedOutputStream bos = null;
                         try {
                              bos = new BufferedOutputStream(new FileOutputStream(outFile));
                              byte[] buffer = new byte[1024];
                              int len = 0;
                              while ( (len = is.read(buffer, 0, buffer.length)) > 0) {
                                   bos.write(buffer, 0, len);
                              }
                              bos.flush();
                         } finally {
                              if (bos != null) { try { bos.close(); } catch (IOException ioe) {} }
                         }
                         zis.closeEntry();
                    }
               }
          } finally {
               if (zis != null) { try { zis.close(); } catch (IOException ioe) {} }
          }
     }

サンプルコード

今回使用したサンプルコードをgithubにアップしました。

https://github.com/hmori/AssetTest

「expand data」ボタンで "assets/data" を内部ストレージ"/data/data/{パッケージ名}/app_data"に展開します。展開中、zipファイルの場合はzip展開してコピーするようにしています。StorageManagerコンストラクタの第3引数をtrueにするとSDカード(/mnt/sdcard/{パッケージ名}/data)へ展開します。

サンプルではassetsにディレクトリ構成で配置して実装していますが、実際にはディレクトリ毎zip圧縮して展開した方がシンプルになると思います。