アプリ内で使用するデータのバックアップやリカバリーを行う場合、ファイルのアクセスが必要となります。
そこで、Androidでファイルの読み込み・書き込み・ファイル一覧を取得する方法について紹介したいと思います。
※iOSのビルド環境がないため、Androidのみとなります。
ファイルアクセスには2つのプラグインが必要です。
Monacaの場合、有料プランの契約が必要となりますので、ご注意ください。
※2020/06/07追記
cordova.plugins.diagnosticプラグイン
アプリが外部ストレージにアクセスする権限を取得するため、requestExternalStorageAuthorizationメソッドを利用します。
これにより、[Download]フォルダなど特定のフォルダにアクセスする権限を取得することができます。
cordova-plugin-fileプラグイン
ファイルの読み込み・書き込みなどを行います。
cordova-plugin-ionic-keyboardプラグイン
入力時にキーボードが表示された場合、キーボードの上にタブバーが表示されるため、キーボード表示・非表示のイベント発火でタブバーの制御を行います。
※ファイルのアクセスには直接関係ありません。
※2020/06/07追記
config.xmlの2行目にあるwidgetに以下のコードを追加します。
config.xml
1 |
xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0" xmlns:vs="http://schemas.microsoft.com/appx/2014/htmlapps" |
config.xmlに以下のコードを追加します。
config.xml
1 2 3 4 5 6 7 8 |
<preference name="AndroidPersistentFileLocation" value="Internal"/> <preference name="AndroidExtraFilesystems" value="files,files-external,documents,sdcard,cache,cache-external,assets,root"/> <platform name="android"> <config-file parent="/*" target="AndroidManifest.xml"> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> </config-file> </platform> |
動作確認のアプリを作成します。
index.html
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"> <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'"> <script src="components/loader.js"></script> <script src="lib/onsenui/js/onsenui.min.js"></script> <link rel="stylesheet" href="components/loader.css"> <link rel="stylesheet" href="lib/onsenui/css/onsenui.css"> <link rel="stylesheet" href="lib/onsenui/css/onsen-css-components.css"> <link rel="stylesheet" href="css/style.css"> <script src="js/file.js"></script> <script src="js/app.js"></script> </head> <body> <ons-page> <ons-tabbar swipeable position="bottom"> <ons-tab page="list.html" label="List" icon="ion-md-list" active></ons-tab> <ons-tab page="rw.html" label="R/W" icon="ion-md-book"></ons-tab> </ons-tabbar> </ons-page> <template id="list.html"> <ons-page id="page-list"> <ons-toolbar> <div class="center">List</div> <div class="right"> <ons-toolbar-button onclick="getFileList('Download')"> <ons-icon icon="ion-md-download"></ons-icon> </ons-toolbar-button> <ons-toolbar-button onclick="getFileList('Pictures')"> <ons-icon icon="ion-md-photos"></ons-icon> </ons-toolbar-button> </div> </ons-toolbar> <ons-list id="list"></ons-list> </ons-page> </template> <template id="rw.html"> <ons-page id="page-pw"> <ons-toolbar> <div class="center">Read / Write</div> </ons-toolbar> <ons-list> <ons-list-item> <textarea id="write-text" class="textarea" rows="5"></textarea> </ons-list-item> <ons-list-item> <ons-row vertical-align="center"> <ons-col width="60%"> <ons-button onclick="writeText()">WRITE</ons-button> </ons-col> <ons-col width="20%"> Append </ons-col> <ons-col width="20%"> <ons-switch id="switch-append"></ons-switch> </ons-col> </ons-row> </ons-list-item> <ons-list-item> <ons-button onclick="readText()">READ</ons-button> </ons-list-item> <ons-list-item> <div id="read-text"></div> </ons-list-item> </ons-list> </ons-page> </template> </body> </html> |
style.css
1 2 3 4 5 6 7 8 9 10 11 12 |
.list-item-name { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; } .list-item-size { text-align: right; } #read-text { white-space: pre-wrap; } |
app.js
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
"use strict"; var errorMessage = function (err) { console.log(window.fn.file.errorMessage(err)); }; // var addListItem = function (list) { var elem = document.getElementById("list"); if (elem) { elem.innerHTML = ""; for (var i = 0; i < list.length; i++) { var item = document.createElement("ons-list-item"); item.innerHTML = "<ons-row vertical-align=\"center\">" + "<ons-col width=\"70%\" class=\"list-item-name\">" + list[i].name + "</ons-col>" + "<ons-col width=\"30%\" class=\"list-item-size\">" + list[i].size + "</ons-col>" + "</ons-row>"; elem.appendChild(item); } } }; // var getFileList = function (directory) { var path = cordova.file.externalRootDirectory + directory + "/"; window.fn.file.authExtStorage().then(function (status) { window.fn.file.getFileList(path).then(function (list) { addListItem(list); }, errorMessage); }, errorMessage); }; // var readText = function () { var elem = document.getElementById("read-text"); if (elem) { elem.innerHTML = ""; window.fn.file.authExtStorage().then(function (status) { var path = cordova.file.externalRootDirectory + "Download" + "/"; window.fn.file.read(path, "test.txt", false).then(function (data) { elem.innerHTML = data; console.log("successful read data"); }, errorMessage); }, errorMessage); } }; // var writeText = function () { var elem = document.getElementById("write-text"); var ap = document.getElementById("switch-append"); if (elem && ap) { window.fn.file.authExtStorage().then(function (status) { var path = cordova.file.externalRootDirectory + "Download" + "/"; window.fn.file.write(path, "test.txt", elem.value, ap.checked).then(function (size) { console.log("successful write data", size); }, errorMessage); }, errorMessage); } }; // var showKeyboard = function (e) { var elem = document.getElementById("tabbar-bottom"); if (elem) { elem.setTabbarVisibility(false); } }; // var hideKeyboard = function (e) { var elem = document.getElementById("tabbar-bottom"); if (elem) { elem.setTabbarVisibility(true); } }; // ons.ready(function () { // keyboard window.addEventListener("keyboardWillShow", showKeyboard, false); window.addEventListener("keyboardDidHide", hideKeyboard, false); }); |
そして、ファイルにアクセスするモジュールfile.jsです。
file.js
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
"use strict"; //************************************************************ // module: file.js // author: otak-lab // use plugin: cordova-plugin-file, cordova.plugins.diagnostic //************************************************************ if (!window.hasOwnProperty("fn")) { window.fn = {}; } window.fn.file = {}; //************************************************************ // authorize external storage //************************************************************ window.fn.file.authExtStorage = function () { return new Promise(function (resolve, reject) { cordova.plugins.diagnostic.requestExternalStorageAuthorization(function (status) { if (status == cordova.plugins.diagnostic.permissionStatus.GRANTED) { resolve(status); } else { reject(status); } }); }); }; //************************************************************ // get file list //************************************************************ window.fn.file.getFileList = function (path) { return new Promise(function (resolve, reject) { var i = 0; var fileEntries = []; var list = []; var errorReject = function (err) { reject(err); }; // read entries var readEntries = function (reader) { return new Promise(function (resolve, reject) { var errorReject = function (err) { reject(err); }; reader.readEntries(function (entries) { if (entries.length > 0) { for (i = 0; i < entries.length; i++) { if (entries[i].isFile) { fileEntries.push(entries[i]); } } readEntries(reader).then(function () { resolve(); }, errorReject); } else { resolve(); } }, errorReject); }); }; // get meta data var getMetaData = function (index) { return new Promise(function (resolve, reject) { var errorReject = function (err) { reject(err); }; if (index < fileEntries.length) { fileEntries[index].getMetadata(function (meta) { list.push({ name: fileEntries[index].name, modifiedTime: meta.modificationTime, size: meta.size, nativeURL: fileEntries[index].nativeURL }); getMetaData(index + 1).then(function () { resolve(); }, errorReject); }, errorReject); } else { resolve(); } }); }; window.resolveLocalFileSystemURL(path, function (fileSystem) { var reader = fileSystem.createReader(); readEntries(reader).then(function () { getMetaData(0).then(function () { resolve(list); }, errorReject); }, errorReject); }, errorReject); }); }; //************************************************************ // read file //************************************************************ window.fn.file.read = function (path, fileName, isBinary) { return new Promise(function (resolve, reject) { var errorReject = function (err) { reject(err); }; window.resolveLocalFileSystemURL(path, function (directoryEntry) { directoryEntry.getFile(fileName, { create: false, exclusive: true }, function (fileEntry) { fileEntry.file(function (file) { var reader = new FileReader(); reader.onloadend = function (e) { resolve(e.target.result); }; reader.onerror = errorReject; if (isBinary) { reader.readAsArrayBuffer(file); } else { reader.readAsText(file); } }, errorReject); }, errorReject); }, errorReject); }); }; //************************************************************ // write file //************************************************************ window.fn.file.write = function (path, fileName, data, isAppend) { return new Promise(function (resolve, reject) { var errorReject = function (err) { reject(err); }; window.resolveLocalFileSystemURL(path, function (directoryEntry) { directoryEntry.getFile(fileName, { create: true, exclusive: false }, function (fileEntry) { fileEntry.createWriter(function (fileWriter) { fileWriter.onwriteend = function (e) { resolve(e.target.length); }; fileWriter.onerror = errorReject; if (isAppend && (fileWriter.length > 0)) { fileWriter.seek(fileWriter.length); } fileWriter.write(data); }); }, errorReject); }, errorReject); }); }; //************************************************************ // file error message //************************************************************ window.fn.file.errorMessage = function (err) { var msg = ""; switch (err.code) { case FileError.NOT_FOUND_ERR: msg = "NOT_FOUND_ERR"; break; case FileError.SECURITY_ERR: msg = "SECURITY_ERR"; break; case FileError.ABORT_ERR: msg = "ABORT_ERR"; break; case FileError.NOT_READABLE_ERR: msg = "NOT_READABLE_ERR"; break; case FileError.ENCODING_ERR: msg = "ENCODING_ERR"; break; case FileError.NO_MODIFICATION_ALLOWED_ERR: msg = "NO_MODIFICATION_ALLOWED_ERR"; break; case FileError.INVALID_STATE_ERR: msg = "INVALID_STATE_ERR"; break; case FileError.SYNTAX_ERR: msg = "SYNTAX_ERR"; break; case FileError.INVALID_MODIFICATION_ERR: msg = "INVALID_MODIFICATION_ERR"; break; case FileError.QUOTA_EXCEEDED_ERR: msg = "QUOTA_EXCEEDED_ERR"; break; case FileError.TYPE_MISMATCH_ERR: msg = "TYPE_MISMATCH_ERR"; break; case FileError.PATH_EXISTS_ERR: msg = "PATH_EXISTS_ERR"; break; default: msg = "UNKNOWN_ERR"; }; return msg; }; |
実際にアプリをビルドしてAndroidにインストールします。
[LIST]タブをタップし、ツールバーの[Download]フォルダのファイル一覧を取得するアイコン、または[Pictures]フォルダのファイル一覧を取得するアイコンをタップします。
アクセスの権限を取得するメッセージが表示されます。[許可]をタップします。
ファイル一覧が表示されます。
[R/W]タブをタップし、メッセージを入力して[WRITE]ボタンをタップすると、[Download]フォルダにtest.txtというファイルが作成されます。
[Appned]スイッチをONにすると、追記でファイルに書き込みされます。
[READ]ボタンをタップすると、test.txtというファイルを読み込み、メッセージを表示します。
「アプリのコードを見せてほしい」「開発を手伝ってほしい」などさまざまなリクエストをたくさんいただいており、であればモジュールfile.jsを有料公開しようと思いましたが、今回は特別に無料公開とします。
そのため、使い方などの説明は省略させていただきます。
MonacaのProプランにてデバッグビルドを行い、Android au SHV40 + Chrome DevToolsにて動作確認済みです。