diff --git a/articles/images/storage-01/inputstream.jpg b/articles/images/storage-01/inputstream.jpg new file mode 100644 index 0000000..362f7cf Binary files /dev/null and b/articles/images/storage-01/inputstream.jpg differ diff --git a/articles/images/storage-01/lesson1-1.png b/articles/images/storage-01/lesson1-1.png new file mode 100644 index 0000000..caf88be Binary files /dev/null and b/articles/images/storage-01/lesson1-1.png differ diff --git a/articles/images/storage-01/lesson1-2.png b/articles/images/storage-01/lesson1-2.png new file mode 100644 index 0000000..2746880 Binary files /dev/null and b/articles/images/storage-01/lesson1-2.png differ diff --git a/articles/images/storage-01/lesson2-1.png b/articles/images/storage-01/lesson2-1.png new file mode 100644 index 0000000..5911121 Binary files /dev/null and b/articles/images/storage-01/lesson2-1.png differ diff --git a/articles/images/storage-01/outputstream.jpg b/articles/images/storage-01/outputstream.jpg new file mode 100644 index 0000000..0b8565c Binary files /dev/null and b/articles/images/storage-01/outputstream.jpg differ diff --git a/articles/images/storage-01/streams.png b/articles/images/storage-01/streams.png new file mode 100644 index 0000000..a7dbf8c Binary files /dev/null and b/articles/images/storage-01/streams.png differ diff --git a/articles/images/storage-02/lesson2-1.png b/articles/images/storage-02/lesson2-1.png new file mode 100644 index 0000000..f654531 Binary files /dev/null and b/articles/images/storage-02/lesson2-1.png differ diff --git a/articles/images/storage-02/rdbms.png b/articles/images/storage-02/rdbms.png new file mode 100644 index 0000000..343b850 Binary files /dev/null and b/articles/images/storage-02/rdbms.png differ diff --git a/articles/images/storage-02/sqlite1.png b/articles/images/storage-02/sqlite1.png new file mode 100644 index 0000000..7504c19 Binary files /dev/null and b/articles/images/storage-02/sqlite1.png differ diff --git a/articles/images/storage-02/sqlite2.png b/articles/images/storage-02/sqlite2.png new file mode 100644 index 0000000..f4eb4fa Binary files /dev/null and b/articles/images/storage-02/sqlite2.png differ diff --git a/articles/images/storage-02/sqlite3.png b/articles/images/storage-02/sqlite3.png new file mode 100644 index 0000000..7b7df1e Binary files /dev/null and b/articles/images/storage-02/sqlite3.png differ diff --git a/articles/images/storage-02/sqlite4.png b/articles/images/storage-02/sqlite4.png new file mode 100644 index 0000000..cc5d3f4 Binary files /dev/null and b/articles/images/storage-02/sqlite4.png differ diff --git a/articles/images/storage-02/sqlite5.png b/articles/images/storage-02/sqlite5.png new file mode 100644 index 0000000..81ef53c Binary files /dev/null and b/articles/images/storage-02/sqlite5.png differ diff --git a/articles/images/storage-02/sqlite6.png b/articles/images/storage-02/sqlite6.png new file mode 100644 index 0000000..3a01b88 Binary files /dev/null and b/articles/images/storage-02/sqlite6.png differ diff --git a/articles/images/storage-02/tablerowcolumn.png b/articles/images/storage-02/tablerowcolumn.png new file mode 100644 index 0000000..c0c7d84 Binary files /dev/null and b/articles/images/storage-02/tablerowcolumn.png differ diff --git a/articles/storage-01.re b/articles/storage-01.re new file mode 100644 index 0000000..779d628 --- /dev/null +++ b/articles/storage-01.re @@ -0,0 +1,379 @@ += ストレージ(1) + +本章では、アプリのデータを端末内に保存したり、読み込んだりする方法を学びます。。本節では、その中でもSharedPreferencesやファイルの扱い方を学びます。 + +== 本節で学べること + + * SharedPreferencesにデータを読み書きできるようになる。 + +アプリの設定情報など、簡単なデータの読み書きができるようになります。 + + * ファイルの読み書きができるようになる。 + +画像など、やや大きめのデータをファイルに保存したり、ファイルから読み込んだりできるようになります。 + +== 本節のキーワード + + * SharedPreferences + * Key-Value形式 + * File + * ストリーム + * 内部ストレージ + * 外部ストレージ + * READ_EXTERNAL_STORAGE + * WRITE_EXTERNAL_STORAGE + +== ストレージとは + +ストレージとは、データを永続的に記録する装置です。PCではハードディスクやSSDがストレージに該当します。Android端末にもストレージはあり、アプリはAPIを介してストレージにデータを記録したりできます。この章では、Androidアプリ開発で主に用いられる3つのストレージAPIについて学びます。 + +== ストレージとメモリ + +まず、データの記録について学びましょう。これまでのアプリ開発で使用した「変数」や「フィールド」は一時的なデータを記録するための仕組みです。変数やフィールドに保存されたデータはアプリ実行中、メモリ上に記録されるため、アプリを終了させたり端末の電源を切ったりすると、記録した内容は失われてしまいます。 + +これに対し、ストレージに記録されたデータはアプリを終了させたり、端末の電源を切っても失われることはありません(経年劣化などで物理的に壊れたりした場合は別ですが)。 + +== ファイル + +ストレージを扱う時の基本単位となるデータのまとまりを「ファイル」と呼びます。画像データが保存されているファイルや、Javaソースコードが保存されているファイルなど、既になじみ深いものだと思います。PCのハードディスクやSSDなどへの読み書きがファイル単位で行われるのと同様に、Androidでもストレージへの読み書きはファイル単位で行われます。 + +===[column]ファイルシステム + +ストレージへの読み書きはファイル単位で行われます。このファイル単位による読み書きを提供するシステムを「ファイルシステム」と呼び、OSによって提供されます。ファイルシステムはWindowsではNTFS、Linuxではext3やext4など、いろいろな種類があります。Androidでは2.3からext4が使用されています。 + +===[/column] + +== Androidアプリ開発で主に利用するストレージAPI + +Androidでは、ストレージに対してデータを作成したり、読み込んだりするためのAPIや仕組みがいくつか用意されています。代表的なものは次の3つです。 + + * SharedPreferences + * SQLiteDatabase + * File + + 本章では、この3つの代表的なAPIの使い方を学びます。 + +また、開発したアプリのデータを他アプリに提供したり、他アプリ(たとえば電話帳など)からデータを取得するための仕組みも用意されています。 + + * ContentProvider + * Storage Access Framework(Android 4.4で追加) + +== SharedPreferences + +SharedPreferencesとは、アプリの設定情報などを保存する仕組みです。SharedPreferencesは、データをKey-Value形式で保存します。Key-Value形式とは、データを「キー」と「値」のペアで保存する形式で、身近なものだと辞書がこれに該当します。 + +=== SharedPreferencesに保存可能なデータの種類 + +SharedPreferencesに値として保存可能なデータの種類は次の6種類です。 + + * String + * int + * long + * float + * boolean + * Set + +=== SharedPreferencesにデータを保存する + +SharedPreferencesにデータを保存するには、次の4ステップを行います。 + + * Context(Activity)のgetSharedPreferences()で、SharedPreferencesオブジェクトを取得する。 + * SharedPreferencesオブジェクトのedit()で、Editorオブジェクトを取得する。 + * Editorオブジェクトのputメソッドで、保存するデータをセットする。 + * apply()を呼び、変更を反映させる。 + +//emlist[SharedPreferencesにデータを保存する]{ +private static final String KEY_NAME = "name"; +private static final String KEY_AGE = "age"; + +SharedPreferences pref = + getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); +SharedPreferences.Editor edit = pref.edit(); +edit.putString(KEY_NAME, name); +edit.putString(KEY_AGE, age); +edit.apply(); +//} + +SharedPreferencesオブジェクトを取得するには、Context(Activity)のgetSharedPreferences()を呼びます。第1引数には、自分で決めたSharedPreferences名を指定します。第2引数はContext.MODE_PRIVATEを指定します。 + +次に、SharedPreferencesオブジェクトのedit()で、データを書き込むためのオブジェクトを取得します。このオブジェクトには、putString()やputInt()など、値の種類に応じたメソッドが用意されています。ここでは"name"というキーに、入力された文字列を値として保存しています。保存したいデータが複数ある場合は、putXXX()をデータの数だけ呼びます。また、同じキーを指定した場合、値は上書きされます。 + +最後に、apply()を呼ぶことでデータの変更を反映させます。これを呼び忘れるとデータは保存されないので注意しましょう。 + +=== SharedPreferencesからデータを読み込む + +SharedPreferencesからデータを読み込むには、次の2ステップを行います。 + + * Context(Activity)のgetSharedPreferences()で、SharedPreferencesオブジェクトを取得する。 + * SharedPreferencesオブジェクトのgetメソッドで、保存したデータを取得する。 + +//emlist[SharedPreferencesからデータを読み込む]{ +SharedPreferences pref = + getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); +String name = pref.getString(KEY_NAME, DEFAULT_NAME); +//} + +SharedPreferencesオブジェクトの取得は書き込み時と同様です。値を読み込むには、値の型に応じたgetXXX()を呼びます。ここでは保存した名前を文字列として取得するため、SharedPreferencesオブジェクトのgetString()を呼んでいます。第1引数で指定したキーに対応する値が無かったり、型が違ったりした場合、getString()は第2引数で指定した値を代わりに返します。 + +=== SharedPreferencesが苦手とすること + +SharedPreferencesはデータの読み書きが数行で実現できる反面、苦手とすることがいくつかあります。 + + * データの検索には向かない + +SharedPreferencesでデータを読み込むには、キーを事前に知っておく必要があります。なので、「年齢が20であるデータを全部取得したい」のような、検索を必要とするデータの保存には向きません。 + + * 複数プロセスで読み書きすると予想外の挙動をすることがある + +残念なことに、SharedPreferencesは複数プロセスから読み書きすると、「アプリで保存したはずの値がServiceで読み込めない」といった挙動をすることがあります。問題となるパターンは基礎の範囲を超えるため、ここでは紹介しません。ですが、「問題がある」ということは頭の片隅に置いておきましょう。 + +=== 練習問題 + +@{lesson1-1}のように、TextView / EditText / Buttonを1つずつ配置してください。そして、次のような動作をするよう、SharedPreferencesを用いて処理を記述し動作確認しましょう。 + + * Buttonをタップすると、EditTextに入力された内容をSharedPreferencesを用いて保存する。 + * アプリ起動時に、SharedPreferencesに値が保存されていれば@{lesson1-2}のようにTextViewの文字列をセットする。 + +//image[lesson1-1][練習問題レイアウト][scale=0.35]{ +//} + +//image[lesson1-2][保存した名前を表示][scale=0.35]{ +//} + +== ファイル + +本節では、Androidアプリでファイルを読み書きする方法を学びます。 + +=== ファイルを保存する場所 + +Androidアプリでファイルを保存できる場所は次の2箇所です。 + + * 内部ストレージ + * (アプリから見た)外部ストレージ + +内部ストレージに保存したファイルは、他のアプリは読み書きできませんが、外部ストレージに保存したファイルは、他のアプリから読み書きできます。なので、作成するファイルの用途に応じて使い分けましょう。 + +=== ファイル(File)とストリーム(Stream) + +ファイル(File)とストリーム(Stream)は、Androidアプリでファイルの読み書き時に使用するクラスです。Javaで既にこれらの使い方を学んでいる方はこの節を読み飛ばしてもかまいません。 + + * ファイル + +ファイルは、ディスク上のファイルやフォルダ(ディレクトリ)を表します。 + + * ストリーム + +ストリームとは、データの流れを扱う「もの」です。流れてきたデータを読み込むストリームをInputStreaemと呼び、データを流すためのストリームをOutputStreamと呼びます。(@{streams}) + +//image[streams][InputStreamとOutputStream][scale=0.15]{ +//} + +=== 内部ストレージにファイルを作成する + +内部ストレージにファイルを作成するには、次の3ステップを行います。 + + * ContextのopenFileOutput()を呼び、OutputStreamオブジェクトを取得する。 + * 取得したOutputStreamオブジェクトに対し、データを書き込む。 + * OutputStreamオブジェクトのclose()を呼び、処理の終了を伝える。 + +//emlist[内部ストレージにファイルを作成する]{ +public class MainActivity extends ActionBarActivity { + // 中略 + + void internalSaveClicked() { + OutputStream out = null; + OutputStreamWriter writer = null; + BufferedWriter bw = null; + try { + out = openFileOutput("myText.txt", Context.MODE_PRIVATE); + writer = new OutputStreamWriter(out); + bw = new BufferedWriter(writer); + + String text = mEdit.getText().toString(); + bw.write(text); + + Toast.makeText(this, R.string.save_done, Toast.LENGTH_SHORT).show(); + + } catch (IOException e) { + Log.e("Internal", "IO Exception " + e.getMessage(), e); + } finally { + try { + if (bw != null) { bw.close(); } + if (writer != null) { writer.close(); } + if (out != null) { out.close(); } + } catch (IOException e) { + Log.e("Internal", "IO Exception " + e.getMessage(), e); + } + } + } +} +//} + +まず、openFileOutput()を呼び、OutputStreamオブジェクトを取得します。第1引数にはファイル名を指定します。第2引数にはContext.MODE_PRIVATEを指定します。 + +続いて、OutputStreamWriterオブジェクトとBufferedWriterオブジェクトを生成します。OutputStreamオブジェクトを用いて直接データを書き込んでもよいのですが、OutputStreamクラスには文字列を効率的に書き込むためのメソッドが用意されていないので、このようにBufferedWriterオブジェクトを用意します。 + +BufferedWriterオブジェクトの生成まで完了したら、後はwrite()でストリームに文字列を書き込みます。 + +ファイルのオープンや書き込みはIOExceptionが発生することがあります。そのため、処理全体をtry-catchで囲みます。また、処理途中でIOExceptionが発生した時も確実に各Streamをクローズする必要があるため、finallyの部分でclose()を呼びます。 + +=== 内部ストレージ内のファイルを読み込む + +内部ストレージ内のファイルを読み込むには、次の3ステップを行います。 + + * ContextのopenFileInput()を呼び、InputStreamオブジェクトを取得する。 + * 取得したInputStreamオブジェクトから、データを読み込む。 + * InputStreamオブジェクトのclose()を呼び、処理の終了を伝える。 + +//emlist[内部ストレージ内のファイルを読み込む]{ +public class MainActivity extends ActionBarActivity { + // 中略 + + void internalLoadClicked() { + InputStream in = null; + InputStreamReader sr = null; + BufferedReader br = null; + try { + in = openFileInput("myText.txt"); + sr = new InputStreamReader(in); + br = new BufferedReader(sr); + + String line; + StringBuilder sb = new StringBuilder(); + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + mEdit.setText(sb.toString()); + + Toast.makeText(this, R.string.load_done, Toast.LENGTH_SHORT).show(); + } catch (IOException e) { + Log.e("Internal", "IO Exception " + e.getMessage(), e); + } finally { + try { + if (br != null) { br.close(); } + if (sr != null) { sr.close(); } + if (in != null) { in.close(); } + } catch (IOException e) { + Log.e("Internal", "IO Exception " + e.getMessage(), e); + } + } + } +} +//} + +まず、openFileInput()を呼び、InputStreamオブジェクトを取得します。第1引数にはファイル名を指定します。 + +続いて、InputStreamReaderとBufferedReaderオブジェクトを生成します。ファイルの書き込みと同様、InputStreamクラスには文字列を効率的に読み込むためのメソッドが用意されていないので、BufferedReaderオブジェクトを用意します。 + +BufferedReaderオブジェクトの生成まで完了したら、readLine()を用いて1行ずつ読み込みます。 + +ファイルの書き込みと同様に、ファイルのオープンや読み込みはIOException@{ioexception}が発生することがあります。そのため、処理全体をtry-catchで囲みます。また、処理途中でIOExceptionが発生した時も確実に各Streamをクローズする必要があるため、finallyの部分でclose()を呼びます。 + +//footnote[ioexception][実行時、入出力に関するエラーが発生した時に投げられる例外です。例えば、存在しないファイルを読み込み用でオープンしようとした時に発生します。] + +=== 外部ストレージにファイルを作成する + +外部ストレージにファイルを作成するには、次の4ステップを行います。 + + * Fileオブジェクトを作成する。 + * 作成したFileオブジェクトを元に、FileOutputStreamオブジェクトを作成する。 + * 作成したFileOutputStreamオブジェクトに対し、データを書き込む。 + * OutputStreamオブジェクトのclose()を呼び、処理の終了を伝える。 + +OutputStreamオブジェクトを作成した後の処理は内部ストレージへの書き込みと同様なので、ここではOutputStreamオブジェクトを作成するまでを解説します。 + +//emlist[外部ストレージにファイルを作成する]{ +public class MainActivity extends ActionBarActivity { + // 中略 + void externalSaveClicked() { + OutputStream out = null; + OutputStreamWriter writer = null; + BufferedWriter bw = null; + try { + File foler = Environment.getExternalStoragePublicDirectory( + "MyDocuments"); + if (!foler.exists()) { + boolean result = foler.mkdir(); + if (!result) { + return; + } + } + File file = new File(foler, "myText.txt"); + out = new FileOutputStream(file); + + // 以下、内部ストレージへの書き込みと同様 + } + } +} +//} + +まず、保存先のフォルダを表すオブジェクトをEnvironment.getExternalStoragePublicDirectory()で取得します。引数にはフォルダの種類を文字列で指定します。ここでは独自に決めたMyDocumentsという種類を指定していますが、システムで用意されている定数を指定することもできます。 + + * Environment.DIRECTORY_ALARMS + * Environment.DIRECTORY_PICTURES + * Environment.DIRECTORY_MUSIC + * Environment.DIRECTORY_MOVIES + +次に、フォルダが実際に存在するかをexists()で確認します。もし存在しない場合はmkdir()でフォルダを作成します。 + +フォルダの作成まで完了したら、newで保存先となるFileオブジェクトを作成します。第1引数には先ほど作成したフォルダオブジェクトを指定します。第2引数にはファイル名を指定します。 + +ファイルオブジェクト作成後、newでFileOutputStreamオブジェクトを作成します。引数には保存先となるFileオブジェクトを指定します。 + +=== 外部ストレージ内のファイルを読み込む + +外部ストレージ内のファイルを読み込むには、次の4ステップを行います。 + + * Fileオブジェクトを作成する。 + * 作成したFileオブジェクトを元に、FileInputStreamオブジェクトを作成する。 + * 取得したFileInputStreamオブジェクトから、データを読み込む。 + * InputStreamオブジェクトのclose()を呼び、処理の終了を伝える。 + +InputStreamオブジェクトを作成した後の処理は内部ストレージ内の読み込みと同様なので、ここではInputStreamオブジェクトを作成するまでを解説します。 + +//emlist[外部ストレージ内のファイルを読み込む]{ +public class MainActivity extends ActionBarActivity { + // 中略 + + void externalLoadClicked() { + InputStream in = null; + InputStreamReader sr = null; + BufferedReader br = null; + try { + File foler = Environment.getExternalStoragePublicDirectory( + "MyDocuments"); + File file = new File(foler, "myText.txt"); + in = new FileInputStream(file); + /// 以下、内部ストレージ内のファイル読み込みと同様 + } + } +} +//} + +Fileオブジェクトの作成までは、外部ストレージにファイルを作成する時と同様です。Fileオブジェクトの作成が完了したら、newでFileInputStreamオブジェクトを作成します。引数には読み込み元を表すFileオブジェクトを指定します。 + +=== Permission + +内部ストレージへのファイル読み書きは特別なPermissionは不要ですが、外部ストレージへのファイル読み書きは他アプリに影響が出るため、Permissionの追加が必要です。 + +開発したAndroidアプリが外部ストレージのファイルを読み込む場合はandroid.permission.READ_EXTERNAL_STORAGEを、外部ストレージのファイルに書き込む場合はandroid.permission.WRITE_EXTERNAL_STORAGEをAndroidManifest.xmlに追加します。 + +//emlist[Permissionの追加]{ + + + + + + + + + +//} + +=== 練習問題 + +@{lesson2-1}のような、入力内容をファイルに保存するメモ帳アプリを作成してみましょう。メモが他のアプリに読まれてしまうのを防ぐため、内部ストレージに保存するようにしてみましょう。 + +//image[lesson2-1][ファイルを使ったメモ帳][scale=0.45]{ +//} \ No newline at end of file diff --git a/articles/storage-02.re b/articles/storage-02.re new file mode 100644 index 0000000..5b36e49 --- /dev/null +++ b/articles/storage-02.re @@ -0,0 +1,581 @@ += ストレージ(2) + +== 本節で学べること + + * 基本的なSQL文が書けるようになる。 + +テーブル作成と、基本的なデータ操作を行うSQL文が書けるようになります。 + + * AndroidのSQLiteを用いたプログラムが書けるようになる。 + +SQLiteOpenHelperを継承したクラスの作成と、SQLiteDatabaseクラスを使ったデータベース操作ができるようになります。 + +== 本節のキーワード + + * SQL + * CREATE TABLE / INSERT / SELECT / UPDATE / DELETE文 + * SQLite + * SQLiteOpenHelper + * SQLiteDatabase + * Cursor + +== SQLite + +SQLiteとは、Android標準で利用可能なデータベース管理システムのことです。ここでは、SQLiteの使い方を学びます。 + +=== リレーショナルデータベース + +SharedPreferencesはKey-Value形式でデータを管理する仕組みでしたが、関係モデル(リレーショナルモデル)に基づいてデータを管理するシステムもあります。このシステムを「リレーショナルデータベースマネジメントシステム(RDBMS)」と呼び、SQLiteはこのRDBMSの一つです。RDBMSによって構築されるデータベースを「リレーショナルデータベース」と呼び、データベース内の関係(リレーション)は一般に「表(テーブル)」と呼ばれます。@{rdbms}に、RDBMSとリレーショナルデータベース・テーブルの関係を示します。 + +//image[rdbms][RDBMSとリレーショナルデータベース・テーブル][scale=0.35]{ +//} + +//footnote[relational-model][関係モデルについて説明すると、それだけで1冊の教科書となってしまいます。ここでは、「データを表のようなかたちで表すモデル」というイメージの紹介にとどめておきます。] + +=== テーブル・列・行 + +リレーショナルデータベースでは、データは@{tablerowcolumn}のように表の形式で保存されています。この表のことを「テーブル」と呼びます。テーブルには名前があり、1つのデータベースに複数のテーブルを格納することができます。また、テーブルの「名前」や「年齢」に相当するものを「列」と呼びます。「列」には名前の他に、数値や文字列などの「型」の情報も持ちます。テーブル内のデータ1つ1つを「行」と呼びます。各データがどのようなフィールドで構成されているかは、「列」を見ればわかります。 + +//image[tablerowcolumn][テーブル・列・行][scale=0.35]{ +//} + +=== SQLとは + +Androidアプリ開発では、端末に対する命令などをJavaという言語で記述しました。これに対し、RDBMSに対する命令(問い合わせ)はSQLと呼ばれる言語で記述します。Android標準のSQLiteに対する問い合わせもSQLで記述します。 + +SQLの文法は、大きく3種類に分けられます。 + + * データ定義言語(DDL) + * データ操作言語(DML) + * データ制御言語(DCL) + +SQLについて詳しく説明すると、それだけで数百ページの教科書となってしまうので、本章ではAndroidでSQLiteを使うために最低限必要となる文法のみ解説します。 + +=== PCでSQLを実行する + +Android実機にはアプリから利用するためのSQLiteソフトウェアが入っていますが、Android SDKにもSQLiteソフトウェアが入っています。これを利用して、次節から解説するSQLをPC上で実行することができます。 + +Windowsでは、¥platform-tools内に「sqlite3.exe」ファイルがあるので、これをダブルクリックで起動します。(@{sqlite1}) + +//image[sqlite1][platform-tools内のsqlite3.exe][scale=0.35]{ +//} + +Mac / Linuxでは、/platform-tools内にsqlite3ファイルがあるので、ターミナルで実行します。 + +sqlite3を起動すると、@{sqlite2}のようなウィンドウが表示されます。終了する時は「.exit」を実行します。Connected to a transient in-memory databaseというメッセージの通り、起動直後はメモリ上のデータベースに接続されます。このデータベースは.exitなどで終了すると同時に消えてしまうので、気軽にSQLを実行して動作を確認したりできます。 + +//image[sqlite2][sqlite3.exeが起動したウィンドウ][scale=0.35]{ +//} + +sqlite3は、初期設定では結果の表示方法がやや不親切なので、次の2つのコマンドを実行しておきます。 + +//emlist[sqlite3の設定変更]{ +.mode column +.header on +//} + +//image[sqlite3][設定を変更する][scale=0.35]{ +//} + +=== テーブルを作る + +まず、SQLに慣れるところから始めましょう。SQLでデータベースにテーブルを作るには、CREATE文を使用します。 + +//emlist[CREATE文]{ +CREATE TABLE <テーブル名>( + 列名1 <型>, + 列名2 <型>, + ...) +//} + +<型>には、SQLiteでは次の5つが指定できます。 + + * TEXT + * INTEGER + * NUMERIC + * REAL + * NONE + +次は、名前と年齢、2つの列をもつuserというテーブルを作る例です。 + +//emlist[userテーブルを作る]{ +CREATE TABLE user( + name TEXT, + age INTEGER) +//} + +sqlite3では、@{sqlite4}のようにCREATE文を実行します。sqlite3では、セミコロンまでを1つのSQLと解釈して実行するため、途中で改行をいれても大丈夫です。途中で改行をいれた場合は、入力行の先頭がsqliteから...に変化します。 + +//image[sqlite4][CREATE文をsqlite3で実行する][scale=0.35]{ +//} + + +=== 練習問題 + +@{lesson2}のようなlectureテーブルを作成するSQLを書いてみましょう。列の型は次のようにします。 + + * _id : INTEGER + * date : INTEGER + * title : TEXT + +//table[lesson2][lectureテーブル]{ +_id date title +--------------------- +1 4/2 開発環境セットアップ +2 4/3 Java基礎 +3 4/4 Java応用 +//} + +=== テーブルに行を追加する + +テーブルができたら、次はそのテーブルにデータ(行)を追加しましょう。行の追加はINSERT文を使います。 + +//emlist[INSERT文]{ +INSERT INTO <テーブル名>(列名1,列名2,...) VALUES(値1,値2,...) +//} + +次は、userテーブルに名前=fkm/年齢=30という行を追加する例です。 + +//emlist[userテーブルに行を追加する]{ +INSERT INTO user(name, age) VALUES('fkm', 30) +//} + +列名と値の対応がとれていれば、次のように列の順序を入れ替えても正しく動作します。 + +//emlist[列の順序を変えてINSERT]{ +INSERT INTO user(age, name) VALUES(30, 'fkm') +//} + +=== テーブルから行を取り出す + +テーブルに追加した行(データ)を取り出す(取得する)には、SELECT文を使います。すべてのパターンを説明すると膨大な量になるので、ここでは代表的なもののみ解説します。 + +//emlist[すべて取得するSELECT文]{ +SELECT * FROM <テーブル名> +//} + +指定した列だけ取得したい場合は、SELECTの後に*ではなく列名をカンマ区切りで指定します。次は、名前列だけ取得するSELECT文です。 + +//emlist[名前だけ取得するSELECT文]{ +SELECT name FROM user +//} + +指定した条件を満たす行(データ)だけ取得したい場合は、FROM <テーブル名> の後にWHEREと条件(これをWHERE句と呼びます)を追加します。次は、年齢が20歳未満の行を取得するためのSELECT文です。 + +//emlist[20歳未満の行を取得するSELECT文]{ +SELECT * FROM user WHERE age < 20 +//} + +条件部分には、次のような演算子が使用できます。 + + * A = B : AとBが等しい + * A <> B : AとBが等しくない + * A < B : AがBより小さい + * A <= B : AがBより小さいか、等しい + * A > B : AがBより大きい + * A >= B : AがBより大きいか、等しい + +また、条件をANDやORでつなげることもできます。次は身長(height)が180より大きく、収入(earnings)が1000以上である行を取得するSELECT文です。 + +//emlist[ANDを使う例]{ +SELECT * FROM worker WHERE height > 180 AND earnings >= 1000 +//} + +@{sqlite5}に、sqlite3でINSERT文とSELECT文を実行した例を示します。 + +//image[sqlite5][INSERTとSELECT文をsqlite3で実行する][scale=0.35]{ +//} + +=== 行を書き換える + +テーブル内の行を書き換える(更新する)には、UPDATE文を使用します。 + +//emlist[UPDATE文]{ +UPDATE <テーブル名> SET 列名1=値,列名2=値,... WHERE 条件 +//} + +WHERE句で指定した条件に一致した行の、指定した列を更新します。WHERE句を忘れると、テーブル内のすべての行を更新してしまうので注意します。次は、id=1の行の収入を300に更新する例です。 + +//emlist[UPDATE文の例]{ +UPDATE worker SET earnings=300 WHERE id=1 +//} + +=== 行を削除する + +テーブル内の行を削除するには、DELETE文を使用します。 + +//emlist[DELETE文]{ +DELETE FROM <テーブル名> WHERE 条件 +//} + +WHERE句で指定した条件に一致した行を削除します。WHERE句を忘れると、テーブル内のすべての行を削除してしまうので特に注意します。次は、id=2の行を削除する例です。 + +//emlist[DELETE文の例]{ +DELETE FROM worker WHERE id=2 +//} + +@{sqlite6}に、sqlite3でUPDATE文とDELETE文を実行した例を示します。DELETE文を実行した後、再度SELECT文で行を取得しようとすると、結果が0件であることがわかります。 + +//image[sqlite6][UPDATEとDELETE文をsqlite3で実行する][scale=0.35]{ +//} + +=== Androidで使う + +SQLに慣れてきたところで、AndroidアプリでSQLiteを使ってみましょう。AndroidでSQLiteを使うには、準備として次の2ステップを行います。 + + * SQLiteOpenHelperクラスを継承したクラスを定義する。 + * 必要な場面で、定義したクラスのオブジェクトを作り、メソッドを呼ぶ。 + +=== SQLiteOpenHelperを継承したクラスを定義する + +まず、SQLiteOpenHelperを継承したMyHelperというクラスを定義します。 + +//emlist[MyHelperクラスを定義する]{ +public class MyHelper extends SQLiteOpenHelper { + private static final String DB_NAME = "my.db"; + private static final int DB_VERSION = 1; + + /** + * コンストラクタ + */ + public MyHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } +} +//} + +SQLiteOpenHelperクラスには引数ありコンストラクタが既に定義されているので、MyHelperクラスにもコンストラクタを用意し、super()を呼びます。第2引数はデータベース名、第3引数はnullを指定します。第4引数は自分で決めたデータベースのバージョンを指定します。現時点では1を指定しておけばよいでしょう。 + +=== onCreate()でテーブル作成SQLを実行する + +SQLiteOpenHelperには2つの抽象メソッドが定義されています。MyHelperクラスはこの2つのメソッドをOverrideしなければなりません。 + + * public void onCreate(SQLiteDatabase db) + * public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) + +onCreate()は、データベースのファイルを作る必要が生じた時に呼ばれます。onUpgrade()は、データベースのバージョンに変化が起きた時(たとえばアプリのバージョンアップなど)に呼ばれます。 + +onCreate()が呼ばれた時に、データベースの初期化としてテーブルを作成するSQLを実行します。SQLを実行するには、onCreate()の引数で渡されるSQLiteDatabaseオブジェクトのexecSQL()を呼びます。 + +//emlist[memoテーブルを作成する]{ +public class MyHelper extends SQLiteOpenHelper { + + public static final String TABLE_NAME = "memo"; + private static final String SQL_CREATE_TABLE = + "CREATE TABLE " + TABLE_NAME + "(" + + Columns._ID + " INTEGER primary key autoincrement," + + Columns.MEMO + " TEXT," + + Columns.CREATE_TIME + " INTEGER," + + Columns.UPDATE_TIME + " INTEGER)"; + + public interface Columns extends BaseColumns { + public static final String MEMO = "memo"; + public static final String CREATE_TIME = "create_time"; + public static final String UPDATE_TIME = "update_time"; + } + + /** + * データベースファイルを作成すべき時に呼ばれる。 + */ + @Override + public void onCreate(SQLiteDatabase db) { + // CREATE文を実行する + db.execSQL(SQL_CREATE_TABLE); + } + + /** + * データベースのバージョン(コンストラクタの第4引数)が + * 変化した時に呼ばれる。 + */ + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // 現時点では何もしない + } +} +//} + +ここでは、データベースファイル作成と同時にCREATE TABLE文を実行し、memoテーブルを作成しています。このため、memoテーブルに行を追加したり、検索しようとした時にテーブルが作成されていないといった問題が起きなくなります。 + +SQLのCREATE TABLE文は間違いが発生しないよう、文字列定数の足し算で構成しています。実際にexecSQL()メソッドで実行されるSQLは次の通りです。 + +//emlist[CREATE TABLE文]{ +CREATE TABLE memo( + _id INTEGER primary key autoincrement, + memo TEXT, + create_time INTEGER, + update_time INTEGER) +//} + +_id列にprimary keyとautoincrementというキーワードが付いています。primary keyは「主キー」と呼び、テーブル内で行を一意に識別するための列であることを示します。ここでは、_id列をprimary keyに指定しているので、_id=1となる行はmemoテーブルに1つしか存在できません。_id=1となる行がmemoテーブルに存在する状態で、別の_id=1となる行を追加しようとするとエラーになります。autoincrementを付けると、行の追加時に自動で1つずつ増やしながら値を設定してくれます。 + +Androidでは、主キーがINTEGER型の_idであるテーブルに対して特定の機能を提供するAPIが存在するため、特に理由が無い場合は主キーを_idにしておきます。 + +=== MyHelperオブジェクトを作る + +MyHelperクラスの定義ができたら、次にオブジェクトを作ります。ここでは、ActivityのonCreate()内で生成し、フィールドにセットしておきます。 + +//emlist[MyHelperオブジェクトを作る]{ +public class MainActivity extends ActionBarActivity { + private MyHelper mHelper; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // MyHelperオブジェクトを作り、フィールドにセット + mHelper = new MyHelper(this); + } +} +//} + +=== 行を追加する + +データベースに行を追加するには、次の3ステップを行います。 + + * MyHelperオブジェクトのgetWritableDatabase()を呼び、SQLiteDatabaseオブジェクトを取得する。 + * 必要なデータを準備し、SQLiteDatabaseオブジェクトのinsert()を呼ぶ。 + * SQLiteDatabaseオブジェクトのclose()を呼び、処理の終了を伝える。 + +//emlist[行を追加する]{ +public class MainActivity extends ActionBarActivity { + // 中略 + + // 引数memoは、画面で入力された内容とします。 + private void insert(String memo) { + SQLiteDatabase db = mMemoDB.getWritableDatabase(); + + // 列に対応する値をセットする + ContentValues values = new ContentValues(); + values.put(MyHelper.Columns.MEMO, memo); + values.put(MyHelper.Columns.CREATE_TIME, System.currentTimeMillis()); + values.put(MyHelper.Columns.UPDATE_TIME, System.currentTimeMillis()); + + // データベースに行を追加する + long id = db.insert(MyHelperDB.TABLE_NAME, null, values); + if (id == -1) { + Log.v("Database", "行の追加に失敗したよ"); + } + + // データベースを閉じる(処理の終了を伝える) + db.close(); + } +} +//} + +追加する行のデータは、ContentValuesオブジェクトのput()で列毎に指定します。ここでは、次のように値をセットしています。 + + * memo列:入力された値 + * create_time列とupdate_time列:現在時刻 + +=== 行を検索する + +次に、追加した行をデータベースを検索して取得しましょう。行の検索は次の7ステップです。 + + 1. MyHelperオブジェクトのgetReadableDatabase()を呼び、SQLiteDatabaseオブジェクトを取得する。 + 2. SQLiteDatabaseオブジェクトのquery()を呼ぶ。検索結果はCursorオブジェクトとして返却される。 + 3. CursorオブジェクトのmoveToFirst()を呼び、読み込み中の位置を検索結果の最初の行に移動させる。これがfalseを返した場合は、検索結果は0件。 + 4. CursorオブジェクトのgetColumnIndex()を呼び、列に対応するインデックスを取得する。 + 5. do - whileを用いて、1行ずつ読み込み位置をずらしながら行のデータを取得する。 + 6. Cursorオブジェクトのclose()を呼び、読み込み終了を伝える。 + 7. SQLiteDatabaseオブジェクトのclose()を呼び、処理の終了を伝える。 + +ややステップ数が多いので、最初にコード全体を示します。 + +//emlist[行の検索]{ +public class MainActivity extends ActionBarActivity { + // 中略 + + private List loadMemo() { + // 1. SQLiteDatabaseオブジェクト取得 + SQLiteDatabase db = mMyHelper.getReadableDatabase(); + + // 2. query()を呼び、検索を行う。 + Cursor cursor = db.query(MyHelper.TABLE_NAME, null, null, null, null, null, + MyHelper.Columns.CREATE_TIME + " ASC"); + // 3. 読み込み位置を先頭にする。falseの場合は結果0件 + if (!cursor.moveToFirst()) { + cursor.close(); + db.close(); + return new ArrayList<>(); + } + + // 4. 列のindex(位置)を取得する + int idIndex = cursor.getColumnIndex(MyHelper.Columns._ID); + int memoIndex = cursor.getColumnIndex(MyHelper.Columns.MEMO); + int createIndex = cursor.getColumnIndex(MyHelper.Columns.CREATE_TIME); + int updateIndex = cursor.getColumnIndex(MyHelper.Columns.UPDATE_TIME); + + // 5. 行を読み込む。 + List list = new ArrayList<>(cursor.getCount()); + do { + Memo item = new Memo(); + item.mId = cursor.getInt(idIndex); + item.mMemo = cursor.getString(memoIndex); + item.mCreateTime = cursor.getLong(createIndex); + item.mUpdateTime = cursor.getLong(updateIndex); + + list.add(item); + // 読み込み位置を次の行に移動させる。 + // 次の行が無い時はfalseを返すのでループを抜ける + } while (cursor.moveToNext()); + + // 6. Cursorを閉じる + cursor.close(); + // 7. データベースを閉じる + db.close(); + + return list; + } +} +//} + +SQLiteDatabaseオブジェクトのquery()を使用すると、自分で複雑なSELECT文を記述することなく検索が行えます。ここでは全件取得し、作成時刻の小さい順(昇順)で並び替えしています。 + +メモリ節約のため、取得する列を指定する場合はquery()の第2引数に列名の配列を指定します。次は、_idとmemo列だけ取得する例です。 + +//emlist[指定した列だけ取得する]{ +String[] columns = { + MyHelper.Columns._ID, + MyHelper.Columns.MEMO +}; +Cursor cursor = db.query(MemoDB.TABLE_NAME, columns, null, null, null, null, + MyHelper.Columns.CREATE_TIME + " ASC"); +//} + +SQLのWHERE句で条件を指定して、特定の行だけ取得するには、第3引数にWHEREの内容を、第4引数には第3引数の?の部分に入れる値を指定します。文章で説明するとイメージしにくいので、_idが指定したものと一致する行だけ取得する例で説明します。 + +//emlist[WHERE句で条件を指定する]{ +// idは引数で渡された値とします。 +String where = MyHelper.Columns._ID + "=?"; +String[] args = { String.valueOf(id) }; +Cursor cursor = db.query(MyHelper.TABLE_NAME, null, where, args, null, null, + MyHelper.Columns.CREATE_TIME + " ASC"); +//} + +ここでは、第3引数は"_id=?"という文字列になっています。=の右辺はユーザーの操作によって実行時に変化するので、?を指定します。そして、第4引数で?の部分に入れる値を指定しています。第3引数が"height > ? AND earnings > ?"のように、?を複数含む場合は、第4引数は?の数と同じ長さの配列にします。 + +勘がいい方は、「なぜ第3引数を"_id=" + idのようにしないんだろう?」と思うかもしれません。なぜこのように?で場所を指定し、第4引数で値を指定するかは後ほど紹介します。 + +query()の結果はCursorオブジェクトで返されます。Cursorオブジェクトには検索結果の行と、読み込み中の行の位置が格納されています。query()直後は読み込み中の行の位置が不定なので、moveToFirst()を呼び、先頭に移動させます。検索結果が0件の場合は、moveToFirst()がfalseを返すので、そこで処理を終了します。 + +結果が1件以上あった場合、次にCursorオブジェクトのgetColumnIndex()で、指定した列名に対するインデックス(位置)を列毎に取得します。これは、Cursorオブジェクトから読み込み中の行の、指定した列のデータを読み込む際、列名ではなく位置を指定するためです。 + +列名に対する位置が取得できたら、いよいよ各行のデータを読み込みます。moveToFirst()で読み込み位置が最初の行に移動しているので、do-whileループを使用します@{cursor-next}。ここでは、1行分のデータを表すMemoオブジェクトを作り、その中に読み込んだデータを格納するようにしています。 + +//footnote[cursor-next][Cursorクラスには「次の要素があるか」を判定するメソッドが無いため、このようにdo-whileループを用いるしかありません。] + +=== 行を更新する + +行の更新は行の追加と似ています。 + + 1. MyHelperオブジェクトのgetWritableDatabase()を呼び、SQLiteDatabaseオブジェクトを取得する。 + 2. 必要なデータを準備し、SQLiteDatabaseオブジェクトのupdate()を呼ぶ。 + 3. SQLiteDatabaseオブジェクトのclose()を呼び、処理の終了を伝える。 + +//emlist[行を更新する]{ +public class MainActivity extends ActionBarActivity { + // 中略 + + private void updateMemo(int id, String memo) { + // 1. SQLiteDatabase取得 + SQLiteDatabase db = mMyHelper.getWritableDatabase(); + + // 2. 更新する値をセット + ContentValues values = new ContentValues(); + values.put(MyHelper.Columns.MEMO, memo); + values.put(MyHelper.Columns.UPDATE_TIME, System.currentTimeMillis()); + + // 更新する行をWHEREで指定 + String where = MyHelper.Columns._ID + "=?"; + String[] args = { String.valueOf(id) }; + + int count = db.update(MyHelper.TABLE_NAME, values, where, args); + if (count == 0) { + Log.v("Edit", "Failed to update"); + } + + // 3. データベースを閉じる + db.close(); + } +} +//} + +SQLのUPDATE文で説明しましたが、行の更新はWHERE句で更新する行を指定します。WHERE部分の指定はupdate()の第3引数と第4で行います。指定方法はquery()の時と同様で、第3引数で?を含む条件を記述し、第4引数で?にいれる値を指定します。update()は呼ぶと、更新に成功した行数を返します。ここでは、_id列が指定された値と一致する行のmemo列とupdate_time列を更新しています。 + +=== 行を削除する + +行の削除はSQLiteDatabaseオブジェクトのdelete()を呼びます。呼ぶまでの手順は行の更新とほぼ同じです。 + + 1. MyHelperオブジェクトのgetWritableDatabase()を呼び、SQLiteDatabaseオブジェクトを取得する。 + 2. 必要なデータを準備し、SQLiteDatabaseオブジェクトのdelete()を呼ぶ。 + 3. SQLiteDatabaseオブジェクトのclose()を呼び、処理の終了を伝える。 + +//emlist[行の削除]{ +public class MainActivity extends ActionBarActivity { + // 中略 + + private void deleteMemo(int id) { + // 1. SQLiteDatabaseを取得 + SQLiteDatabase db = mMyHelper.getWritableDatabase(); + + // 2. 削除する行の条件を設定 + String where = MyHelper.Columns._ID + "=?"; + String[] args = { String.valueOf(id) }; + + int count = db.delete(MyHelper.TABLE_NAME, where, args); + if (count == 0) { + Log.v("Edit", "Failed to delete"); + } + + // 3. データベースを閉じる + db.close(); + } +} +//} + +こちらもupdate()と同様、どの行を削除するかをdelete()の第2引数と第3引数で指定します。指定方法もupdate()の時と同様です。delete()は呼ぶと削除した行数を返します。ここでは、_id列が指定した値と同じ行を削除しています。 + +=== なぜWHERE句に?を使用するか + +query()やupdate()などは、SQLiteDatabaseクラスの内部でSQLを組み立てた上で実行されます。この時、WHERE句で指定した条件はSQL組み立て時にそのまま使用されます。もし、WHERE句に?が含まれていた場合は、SQLとして意味が変わらないよう、順に値が割り当てられます。 + +では、WHERE句の条件に?を使用せず、"name=" + nameのように文字列を連結したものを渡した場合を考えてみます。変数nameはString型として、"fkm"が入っていた場合、最終的に次のようなSQLが実行されるでしょう。 + +//emlist[nameにfkmが入っていた場合]{ +SELECT * FROM user WHERE name=fkm +//} + +もし、変数nameに"a OR 1=1"が入っていた場合、次のようなSQLが実行されてしまいます。 + +//emlist[nameにa OR 1=1が入っていた場合]{ +SELECT * FROM user WHERE name=a OR 1=1 +//} + +これは、「userテーブル内で、nameがaと等しいか、1=1が真となる行をすべて取得しなさい」という意味になります。この場合、1=1は常に真となるので、userテーブル内のすべての行が取得できてしまいます。 + +?を含む文字列を条件として指定し、その次の引数で?に割り当てる値を指定した場合、次のようなSQLが実行され、「全件取得してしまう」のような意図しない動作を防ぐことができます。 + +//emlist[条件に?を用いた場合]{ +SELECT * FROM user WHERE name='a OR 1=1' +//} + +このように、WHERE句の指定に?を使用せず、入力値を連結したものを使用した場合、値によって意図していないSQLが実行されてしまうことがあります。上記のnameのような変数にSQLの動作を変える文字列を入れることで、不正にデータを取得したり、データベースを破壊したりすることを「SQLインジェクション」と呼びます。 + +=== 練習問題 + +@{lesson2-1}のようなレイアウトを作成し、入力されたToDoをSQLiteに保存するミニアプリを作ってみましょう。 + + * 「保存」ボタンを押すと、入力されたToDoをtodoテーブルにinsertする。 + * 「取得」ボタンを押すと、todoテーブルの内容を全件取得し、TextViewに1行ずつ表示する。 + +//image[lesson2-1][ToDoを保存するアプリ][scale=0.35]{ +//} + +テーブル定義などはこれまでの内容を参考にして、自分で決めてみましょう。 + +== まとめ + +本章では、Androidアプリ開発で主に利用する3つのAPIの使い方を学びました。アプリが扱うデータの性質に応じて、適切な仕組みを選択できるようになりましょう。 + + diff --git a/code/storage/FileSample/.gitignore b/code/storage/FileSample/.gitignore new file mode 100644 index 0000000..e7b202b --- /dev/null +++ b/code/storage/FileSample/.gitignore @@ -0,0 +1,9 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +.idea/* +*.iml + diff --git a/code/storage/FileSample/app/.gitignore b/code/storage/FileSample/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/code/storage/FileSample/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/code/storage/FileSample/app/build.gradle b/code/storage/FileSample/app/build.gradle new file mode 100644 index 0000000..ce14237 --- /dev/null +++ b/code/storage/FileSample/app/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion "21.0.2" + + defaultConfig { + applicationId "jp.androidopentextbook.storage.filesample" + minSdkVersion 10 + targetSdkVersion 21 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:21.0.2' + compile 'com.jakewharton:butterknife:6.0.0' +} diff --git a/code/storage/FileSample/app/proguard-rules.pro b/code/storage/FileSample/app/proguard-rules.pro new file mode 100644 index 0000000..968b913 --- /dev/null +++ b/code/storage/FileSample/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/fkm/software/android-sdk-macosx/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/code/storage/FileSample/app/src/androidTest/java/jp/androidopentextbook/storage/filesample/ApplicationTest.java b/code/storage/FileSample/app/src/androidTest/java/jp/androidopentextbook/storage/filesample/ApplicationTest.java new file mode 100644 index 0000000..55e09af --- /dev/null +++ b/code/storage/FileSample/app/src/androidTest/java/jp/androidopentextbook/storage/filesample/ApplicationTest.java @@ -0,0 +1,13 @@ +package jp.androidopentextbook.storage.filesample; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/code/storage/FileSample/app/src/main/AndroidManifest.xml b/code/storage/FileSample/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..598efce --- /dev/null +++ b/code/storage/FileSample/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/code/storage/FileSample/app/src/main/java/jp/androidopentextbook/storage/filesample/MainActivity.java b/code/storage/FileSample/app/src/main/java/jp/androidopentextbook/storage/filesample/MainActivity.java new file mode 100644 index 0000000..cfc9027 --- /dev/null +++ b/code/storage/FileSample/app/src/main/java/jp/androidopentextbook/storage/filesample/MainActivity.java @@ -0,0 +1,170 @@ +package jp.androidopentextbook.storage.filesample; + +import android.content.Context; +import android.os.Environment; +import android.support.v7.app.ActionBarActivity; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.EditText; +import android.widget.Toast; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import butterknife.ButterKnife; +import butterknife.InjectView; +import butterknife.OnClick; + + +public class MainActivity extends ActionBarActivity { + + @InjectView(R.id.edit) + EditText mEdit; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + ButterKnife.inject(this, this); + } + + @OnClick(R.id.button_internal_load) + void internalLoadClicked() { + InputStream in = null; + InputStreamReader sr = null; + BufferedReader br = null; + try { + in = openFileInput("myText.txt"); + sr = new InputStreamReader(in); + br = new BufferedReader(sr); + + String line; + StringBuilder sb = new StringBuilder(); + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + mEdit.setText(sb.toString()); + + Toast.makeText(this, R.string.load_done, Toast.LENGTH_SHORT).show(); + } catch (IOException e) { + Log.e("Internal", "IO Exception " + e.getMessage(), e); + } finally { + try { + if (br != null) { br.close(); } + if (sr != null) { sr.close(); } + if (in != null) { in.close(); } + } catch (IOException e) { + Log.e("Internal", "IO Exception " + e.getMessage(), e); + } + } + } + + @OnClick(R.id.button_internal_save) + void internalSaveClicked() { + OutputStream out = null; + OutputStreamWriter writer = null; + BufferedWriter bw = null; + try { + out = openFileOutput("myText.txt", Context.MODE_PRIVATE); + writer = new OutputStreamWriter(out); + bw = new BufferedWriter(writer); + + String text = mEdit.getText().toString(); + bw.write(text); + + Toast.makeText(this, R.string.save_done, Toast.LENGTH_SHORT).show(); + + } catch (IOException e) { + Log.e("Internal", "IO Exception " + e.getMessage(), e); + } finally { + try { + if (bw != null) { bw.close(); } + if (writer != null) { writer.close(); } + if (out != null) { out.close(); } + } catch (IOException e) { + Log.e("Internal", "IO Exception " + e.getMessage(), e); + } + } + } + + @OnClick(R.id.button_external_load) + void externalLoadClicked() { + InputStream in = null; + InputStreamReader sr = null; + BufferedReader br = null; + try { + File foler = Environment.getExternalStoragePublicDirectory("MyDocuments"); + File file = new File(foler, "myText.txt"); + in = new FileInputStream(file); + sr = new InputStreamReader(in); + br = new BufferedReader(sr); + + String line; + StringBuilder sb = new StringBuilder(); + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + mEdit.setText(sb.toString()); + + Toast.makeText(this, R.string.load_done, Toast.LENGTH_SHORT).show(); + } catch (IOException e) { + Log.e("External", "IO Exception " + e.getMessage(), e); + } finally { + try { + if (br != null) { br.close(); } + if (sr != null) { sr.close(); } + if (in != null) { in.close(); } + } catch (IOException e) { + Log.e("External", "IO Exception " + e.getMessage(), e); + } + } + } + + @OnClick(R.id.button_external_save) + void externalSaveClicked() { + OutputStream out = null; + OutputStreamWriter writer = null; + BufferedWriter bw = null; + try { + File foler = Environment.getExternalStoragePublicDirectory("MyDocuments"); + if (!foler.exists()) { + boolean result = foler.mkdir(); + if (!result) { + return; + } + } + File file = new File(foler, "myText.txt"); + out = new FileOutputStream(file); + writer = new OutputStreamWriter(out); + bw = new BufferedWriter(writer); + + String text = mEdit.getText().toString(); + bw.write(text); + + Toast.makeText(this, R.string.save_done, Toast.LENGTH_SHORT).show(); + } catch (IOException e) { + Log.e("External", "IO Exception " + e.getMessage(), e); + } finally { + try { + if (bw != null) { bw.close(); } + if (writer != null) { writer.close(); } + if (out != null) { out.close(); } + } catch (IOException e) { + Log.e("External", "IO Exception " + e.getMessage(), e); + } + } + } + +} diff --git a/code/storage/FileSample/app/src/main/res/drawable-hdpi/ic_launcher.png b/code/storage/FileSample/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..96a442e Binary files /dev/null and b/code/storage/FileSample/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/code/storage/FileSample/app/src/main/res/drawable-mdpi/ic_launcher.png b/code/storage/FileSample/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..359047d Binary files /dev/null and b/code/storage/FileSample/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/code/storage/FileSample/app/src/main/res/drawable-xhdpi/ic_launcher.png b/code/storage/FileSample/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..71c6d76 Binary files /dev/null and b/code/storage/FileSample/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/code/storage/FileSample/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/code/storage/FileSample/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..4df1894 Binary files /dev/null and b/code/storage/FileSample/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/code/storage/FileSample/app/src/main/res/layout/activity_main.xml b/code/storage/FileSample/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..624330e --- /dev/null +++ b/code/storage/FileSample/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,75 @@ + + + + + + +