Skip to content

Instantly share code, notes, and snippets.

@motemen
Last active May 9, 2020 10:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save motemen/5653098 to your computer and use it in GitHub Desktop.
Save motemen/5653098 to your computer and use it in GitHub Desktop.
Saving Data, Interecting with Other Apps

データを保存する

Saving Data

  • だいたいのアプリはデータを保存する必要がある
    • onPause() でユーザの進捗を保存する
    • アプリの設定を保存する
    • ファイルやデータベースに巨大なデータを保存する
  • 以下で見ていく内容:
    • Shared preference ファイルに単純なデータ型の key-value ペアを保存する
    • アンドロイドのファイルシステムでファイルを保存する
    • SQLite によるデータベースの利用

Key-value の集合を保存する

Saving Key-Value Sets

  • 比較的小さな key-value ペアでいい場合は SharedPreferences を使う
    • SharedPreferences は key-value ペアを格納したファイルに対応し、それを読み書きするメソッドを提供
    • ファイルはフレームワークが管理する
    • ファイルはプライベートにも共有にもできる

SharedPreferences を取得する

  • getSharedPreferences(String, int): 複数の shared preference ファイルが必要なとき使う(第一引数で区別する)。Context から呼ぶ
  • getPreferences(int): ひとつの shared preference ファイルでいいときはこちらを使う。Activity から呼ぶ
  • Fragment から呼ぶ例:
Context context = getActivity();
SharedPreferences sharedPref = context.getSharedPreferences(
        getString(R.string.preference_file_key), Context.MODE_PRIVATE);
  • Shared preference ファイルに名前をつけるときは自分のアプリだと分かる名前にしなければいけない(例: "com.example.myapp.PREFERENCE_FILE_KEY"
  • Context.MODE_PRIVATEContext.MODE_WORLD_READABLE とかにすることで他のアプリからも利用可能になる

Shared preference の読み

SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(getString(R.string.saved_high_score), newHighScore);
editor.commit();

Shared preference の書き

  • getXXX() する
  • デフォルト値の指定も可能
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
int defaultValue = getResources().getInteger(R.string.saved_high_score_default);
long highScore = sharedPref.getInt(getString(R.string.saved_high_score), defaultValue);

ファイルを保存する

Saving Files

  • Android には他のプラットフォームのディスクベースのものに似たファイルシステムがある
  • File API を使用
    • 大きなデータをシーケンシャルに読み書きする場合に適している
  • 前提知識
    • Linux のファイルシステム
    • java.io API

内部ストレージか、外部ストレージか

  • Android には内部ストレージと外部ストレージがある
    • 昔はマイクロ SD カードなどが外部ストレージに相当したが、今は内蔵のストレージを内部と外部に分けているデバイスもある(なので外部ストレージだからといってリムーバブルだとは限らない)
  • 内部ストレージ
    • 常に利用可能
    • 保存されたファイルはデフォルトでは自分のアプリによってのみアクセス可能
    • アプリのアンインストール時、内部ストレージにあるアプリのファイルがすべて削除される
    • ファイルをユーザにも他のアプリにも触られたくない場合に適している
  • 外部ストレージ
    • 常に利用可能というわけではない(外部ストレージを USB メモリとしてマウントしている時や外したとき)
    • 誰でも読むことができる(world-readable)
    • アプリのアンインストール時、getExternalFilesDir(String) で得られるディレクトリに置かれたファイルがすべて削除される
    • ファイルのアクセス制御の必要がない時、他のアプリに共有したいとき、ユーザにアクセスさせたいときなどに適している
  • android:installLocation で外部ストレージにアプリをインストールさせることもできる(通常は内部ストレージ)。参照: App Install Location

外部ストレージへのパーミッションの取得

  • 外部ストレージに書き込みを行うときはマニフェストファイルで WRITE_EXTERNAL_STORAGE を要求する必要がある
    • 外部ストレージの読み込みは現在のところデフォルトで可能だが将来的に変わりうるので今のうちから READ_EXTERNAL_STORAGE を要求しておくとよい(WRITE_EXTERNAL_STORAGE を指定しているなら不要)
<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>
  • 内部ストレージに保存するのにパーミッションの要求は不要

内部ストレージにファイルを保存する

  • ディレクトリを取得する

    • getFilesDir()
      • アプリ用の内部ディレクトリを表わす File を取得する
    • getCacheDir()
      • アプリの一時キャッシュファイル用の内部ディレクトリを表わす File を取得する
      • システムのストレージ容量が減ってくると警告なしに削除されるので適当にサイズ制限をするなど管理しておくこと
  • 例: File を作る

File file = new File(context.getFilesDir(), filename);
String filename = "myfile";
String string = "Hello world!";
FileOutputStream outputStream;

try {
  outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
  outputStream.write(string.getBytes());
  outputStream.close();
} catch (Exception e) {
  e.printStackTrace();
}
public File getTempFile(Context context, String url) {
    File file;
    try {
        String fileName = Uri.parse(url).getLastPathSegment();
        file = File.createTempFile(fileName, null, context.getCacheDir());
    catch (IOException e) {
        // Error while creating file
    }
    return file;
}
  • Note: 内部ストレージディレクトリはアプリのパッケージ名で決まるので、内部ストレージのファイルのパーミッションを読み込み可にしてもディレクトリ名が分かっていないと他のアプリから読み込むことはできない

外部ストレージにファイルを保存する

/* 外部ストレージが読み書き可能であることを調べる */
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

/* 外部ストレージが読み込み可能であることをチェック */
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}
  • 外部ストレージのファイルは 2 つに分けられる

    • パブリック
      • ユーザや他のアプリから自由に利用可能
      • アプリのアンイストール時に削除されない
      • アプリで撮った写真やダウンロードしたファイルなど
    • プライベート
      • アプリに所属するファイルで、アプリのアンイストール時に削除されるべきもの
      • 技術的には外部から利用可能だが、現実的にそうしても意味がないようなもの
      • アプリのダウンロードされた追加リソースとか、一次的なメディアファイルなど
  • 外部ストレージにパブリックなファイルを保存したいときは、Environment.getExternalStoragePublicDirectory(String) を使用

    • 第一引数には DIRECTORY_MUSICDIRECTORY_PICTURES など、保存したいファイルの種類を指定する
      • 他のパブリックなファイルとあわせて整理できるように
public File getAlbumStorageDir(String albumName) {
    // Get the directory for the user's public pictures directory. 
    File file = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES), albumName);
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}
  • 外部ストレージにプライベートなファイルを保存したいときは、Context#getExternalFilesDir(String) に作成したいディレクトリの名前を与えて呼び出す
    • アプリの外部ストレージファイルを格納する親ディレクトリ(アプリのアンイストール時に削除される)の下にディレクトリが作られる
    • DIRECTORY_PICTURES など)定義済のディレクトリ名が用途に沿わない場合は null を与えることで親ディレクトリ自身を取得できる
public File getAlbumStorageDir(Context context, String albumName) {
    // Get the directory for the app's private pictures directory. 
    File file = new File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName);
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}
  • ディレクトリを作る際、パブリックであるかプライベートであるかに関わらず DIRECTORY_PICTURES などの定数を使うことが重要
    • これらの定数値を使うことで、システムに正しく扱われることが保証される
    • 定義済の定数(抜粋)
      • DIRECTORY_ALARMS
      • DIRECTORY_DOWNLOADS
      • DIRECTORY_MOVEIS
      • DIRECTORY_MUSIC
      • DIRECTORY_NOTIFICATIONS
      • DIRECTORY_PICTURES
      • DIRECTORY_RINGTONES

空き領域の問い合わせ

  • getFreeSpace()getTotalSpace() を使うことで、(ファイル書き込み時の)例外に頼らず十分な空き領域があるかどうかを知ることができる
  • getFreeSpace が返す分のデータ量を書き込めることを保証するものではない
    • 書き込む分に加えて数 MB の余裕があるとか、使用量が 90% 未満であればおそらく大丈夫
  • Note: どのくらいファイル容量が必要になるか分からないときなど、空き領域を問い合わせることは必ずしも必要ではない(そういう時はやはり例外を使う)

ファイルの削除

SQL データベースにデータを保存する

スキーマとコントラクトを定義する

  • スキーマに対応するコントラクト(contract)クラスというクラスを定義する

    • コントラクトクラスは URI、テーブル、カラム名に対応する定数を持つクラス
    • データベース全体にグローバルな定義をクラスのルートに置き、テーブルに対応する定義を内部クラスに持たせる
  • Note: BaseColumns インターフェースを実装しておくと cursor アダプタなどが期待する _ID というフィールドを継承することができる。必須ではないがこうしておくと Android フレームワークと協調できる

  • 例: あるテーブルのテーブル名とカラム名を定義する

public static abstract class FeedEntry implements BaseColumns {
    public static final String TABLE_NAME = "entry";
    public static final String COLUMN_NAME_ENTRY_ID = "entryid";
    public static final String COLUMN_NAME_TITLE = "title";
    public static final String COLUMN_NAME_SUBTITLE = "subtitle";
    ...
}

SQL ヘルパを使ってデータベースを作成する

  • 内部ストレージにファイルを保存したときと同様に、データベースはアプリに紐付いたプライベートな場所に保存される
  • SQLiteOpenHelper クラス
public class FeedReaderDbHelper extends SQLiteOpenHelper {
    // スキーマを変更した場合、このバージョンをインクリメントする
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "FeedReader.db";

    public FeedReaderDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_ENTRIES);
    }
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // この例においては DB に入るデータはキャッシュなので全部消し去る
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        onUpgrade(db, oldVersion, newVersion);
    }
}
  • いわゆるマイグレーション的なものを実現している?

データベースに情報を格納する

nullColumnHack optional; may be null. SQL doesn't allow inserting a completely empty row without naming at least one column name. If your provided values is empty, no column names are known and an empty row can't be inserted. If not set to null, the nullColumnHack parameter provides the name of nullable column name to explicitly insert a NULL into in the case where your values is empty.

// Gets the data repository in write mode
SQLiteDatabase db = mDbHelper.getWritableDatabase();

// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID, id);
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_CONTENT, content);

// Insert the new row, returning the primary key value of the new row
long newRowId;
newRowId = db.insert(
         FeedReaderContract.FeedEntry.TABLE_NAME,
         FeedReaderContract.FeedEntry.COLUMN_NAME_NULLABLE,
         values);

データベースから情報を読み込む

SQLiteDatabase db = mDbHelper.getReadableDatabase();

// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
    FeedReaderContract.FeedEntry._ID,
    FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE,
    FeedReaderContract.FeedEntry.COLUMN_NAME_UPDATED,
    ...
    };

// How you want the results sorted in the resulting Cursor
String sortOrder =
    FeedReaderContract.FeedEntry.COLUMN_NAME_UPDATED + " DESC";

Cursor c = db.query(
    FeedReaderContract.FeedEntry.TABLE_NAME,  // The table to query
    projection,                               // The columns to return
    selection,                                // The columns for the WHERE clause
    selectionArgs,                            // The values for the WHERE clause
    null,                                     // don't group the rows
    null,                                     // don't filter by row groups
    sortOrder                                 // The sort order
    );
  • Cursor オブジェクトの指す行を参照するには
cursor.moveToFirst();
long itemId = cursor.getLong(
    cursor.getColumnIndexOrThrow(FeedReaderContract.FeedEntry._ID)
);

データベースから情報を削除する

// Define 'where' part of query.
String selection = FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
// Specify arguments in placeholder order.
String[] selectionArgs = { String.valueOf(rowId) };
// Issue SQL statement.
db.delete(table_name, selection, selectionArgs);

データベースを更新する

SQLiteDatabase db = mDbHelper.getReadableDatabase();

// New value for one column
ContentValues values = new ContentValues();
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, title);

// Which row to update, based on the ID
String selection = FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selectionArgs = { String.valueOf(rowId) };

int count = db.update(
    FeedReaderDbHelper.FeedEntry.TABLE_NAME,
    values,
    selection,
    selectionArgs);

他のアプリとやりとりする

Interact with Other Apps

  • アプリはいくつかのアクティビティを持つ
  • ユーザを他のアクティビティに連れていくには Intent を使用する
  • インテントには 明示的な (explicit)ものと 暗黙の (implicit)ものとがある

ユーザを他のアプリに送る

Sending the User to Another App

  • Android の重要な機能のひとつ: アクションに応じてユーザをアプリからアプリへ遷移させる
    • 地図を見せたいときにマップアプリを開くなど
  • Building Your First App で見たとおりユーザにアクティビティを見せるにはクラス名を指定した明示的なインテントを利用した
  • 一方、地図を見せるなどのアクションに基くものは暗黙の intent
  • ここではアクションに応じた暗黙のインテントを作成し、他のアプリでアクティビティを起動する方法を見る

暗黙のインテントを構築する

  • 暗黙のインテントは起動したいクラス名ではなく、アクションを宣言する

    • 見る(view)、編集する(edit)、送る(send)、得る(get)など
    • 多くの場合インテントにはデータが付属する
      • 見たいアドレスや送りたいメッセージなど
      • データは Uri だったり他のデータ型であったり何もなかったり
  • データが Uri である場合はコンストラクタで渡すことができる

    • 例: 電話をかける
      • この Intent を startActivity(Intent) に渡すと、電話アプリが起動して電話をかけることができる
Uri number = Uri.parse("tel:5551234");
Intent callIntent = new Intent(Intent.ACTION_DIAL, number);
  • 追加のデータが必要な種類の暗黙のインテントでは、putExtra(String,*) で 1 つ以上のデータを与える

  • デフォルトではインテントに与えられた Uri で MIME タイプが決まるが、Uri を与えない場合は setType(String) で MIME タイプの設定ができる

    • Note: インテントを起動するときは MIME タイプを設定するなど情報を詳細にしておくのが重要
      • 画像を表示(ACTION_VIEW)したいのに MIME タイプに "image/*" を設定していないと、地図アプリのようなものも呼ばれうる
  • 例: 添付ファイルつきのメールを送る

Intent emailIntent = new Intent(Intent.ACTION_SEND);
// The intent does not have a URI, so declare the "text/plain" MIME type
emailIntent.setType(HTTP.PLAIN_TEXT_TYPE);
emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"jon@example.com"}); // recipients
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Email subject");
emailIntent.putExtra(Intent.EXTRA_TEXT, "Email message text");
emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse("content://path/to/email/attachment"));
// You can also attach multiple items by passing an ArrayList of Uris
  • 例: カレンダーイベントを作成する(API level 14 以上)
Intent calendarIntent = new Intent(Intent.ACTION_INSERT, Events.CONTENT_URI);
Calendar beginTime = Calendar.getInstance().set(2012, 0, 19, 7, 30);
Calendar endTime = Calendar.getInstance().set(2012, 0, 19, 10, 30);
calendarIntent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, beginTime.getTimeInMillis());
calendarIntent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endTime.getTimeInMillis());
calendarIntent.putExtra(Events.TITLE, "Ninja class");
calendarIntent.putExtra(Events.EVENT_LOCATION, "Secret dojo");

インテントを受け取るアプリがあることを確認する

  • いくつかのインテントは組み込みのアプリ(電話、メール、カレンダーなど)に処理されることが保証されているが、intent 起動前に確認のステップを挟むべき
    • Caution: 処理できるアプリのないインテントを起動すると、アプリはクラッシュする
  • queryIntentActivities(Intent,int) を使用する

インテントでアクティビティを起動する

  • startActivity(Intent) でインテントをシステムに送ることができる
    • 2 つ以上のアプリがそのインテントに対応していればダイアログが表示される
    • 1 つしかなければ即座にそのアプリが起動する

アプリ選択ダイアログ(app chooser)を表示する

  • 「共有」のようにデフォルトのアクションがない場合、明示的にアプリ選択ダイアログを表示できる
  • createChooser(Intent,CharSequence) でインテントを作成し、それを起動する
  • ACTION_CHOOSER アクションということになるらしい
Intent intent = new Intent(Intent.ACTION_SEND);
...

// Always use string resources for UI text. This says something like "Share this photo with"
String title = getResources().getText(R.string.chooser_title);
// Create and start the chooser
Intent chooser = Intent.createChooser(intent, title);
startActivity(chooser);

アクティビティから結果を取得する

Getting a Result from an Activity

  • startActivityForResult(Intent,int) で起動したアクティビティから値を受け取ることができる
    • カメラアプリを起動して写真を結果として受け取る、など
  • 当然、起動される側のアプリは結果を返すように作られている必要がある
  • Note: 明示的なインテントでも startActivityForResult 可能

アクティビティを起動する

  • startActivity の場合と殆ど一緒だが、startActivityForResult にはリクエストコードなる引数をも渡す必要がある
    • このリクエストコードは結果の Intent を受け取る際に一緒に渡される
static final int PICK_CONTACT_REQUEST = 1;  // The request code
...
private void pickContact() {
    Intent pickContactIntent = new Intent(Intent.ACTION_PICK, Uri.parse("content://contacts"));
    pickContactIntent.setType(Phone.CONTENT_TYPE); // Show user only contacts w/ phone numbers
    startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);
}

結果を受け取る

  • ユーザが起動されたアクティビティを完了して戻ったとき、呼び出し側の onActivityResult メソッドが呼ばれる
    • 第 1 引数: startActivityForResult に渡したリクエストコード
    • 第 2 引数: 呼ばれたアクティビティの結果コード(RESULT_OK または RESULT_CANCELED
    • 第 3 引数: 結果を持つ Intent
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // Check which request we're responding to
    if (requestCode == PICK_CONTACT_REQUEST) {
        // Make sure the request was successful
        if (resultCode == RESULT_OK) {
            // The user picked a contact.
            // The Intent's data Uri identifies which contact was selected.

            // Do something with the contact here (bigger example below)
        }
    }
}
  • この例の場合、結果の Intent には選択された連絡先情報を表すコンテント Uri が含まれている
    • 結果を正しく処理するには、結果の Intent がどのようなフォーマットになっているかを理解している必要がある

他のアプリからアクティビティを起動できるようにする

Allowing Other Apps to Start Your Activity

  • 今まで見た話のもう一方の側、intent を受け取る方
  • マニフェストファイルの <activity> 要素に <intent-filter> 要素を追加する
  • アプリのインストール時にシステムがアプリのインテントフィルタを見て、内部カタログを更新する

インテントフィルタを追加する

  • インテントフィルタでは、アクティビティがどのようなアクションおよびデータを処理できるかをできるだけ詳細に指定する
  • システムは、以下の基準を満たすアクティビティを起動する
    • アクション
      • 実行したいアクションを示す文字列。通常はプラットフォームにより定義された ACTION_SENDACTION_VIEW など
      • インテントフィルタの <action> で指定する
    • データ
      • インテントに紐付けられるデータの詳細
      • <data> で指定する。1 つ以上の属性で MIME タイプや URI プレフィクスやスキームなどを指定できる
      • Note: アクティビティが URI ではなく追加データを処理するような場合には URI に関する指定は不要、MIME タイプに関する指定だけすればよい
    • カテゴリ
      • インテントを扱うアクティビティのさらなる性質を指定
      • あまり使われない(デフォルトでは CATEGORY_DEFAULT
      • <category> で指定する
      • CATEGORY_CAR_MODE とかあるらしい
      • Note: 暗黙のインテントを受け取るには、CATEGORY_DEFAULT を指定している必要がある
  • 例: ACTION_SEND インテントでデータがテキストまたは画像のものを処理するインテントフィルタ
<activity android:name="ShareActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
        <data android:mimeType="image/*"/>
    </intent-filter>
</activity>
  • <intent-filter> には先に紹介したどの子要素も、複数指定してよい
    • アクションとデータのペアが挙動の上で両立しない場合は別々のフィルタとして指定
  • ちなみに ACTION_SEND の詳細についてはあとで(Receiving Content from Other Apps

アクティビティでインテントを処理する

  • アクティビティの起動時に getIntent() でアクティビティを起動させたインテントを取得することができる
    • いつでもできるけど、一般的には onCreate か onStart で行うべき
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.main);

    // Get the intent that started this activity
    Intent intent = getIntent();
    Uri data = intent.getData();

    // Figure out what to do based on the intent type
    if (intent.getType().indexOf("image/") != -1) {
        // Handle intents with image data ...
    } else if (intent.getType().equals("text/plain")) {
        // Handle intents with text ...
    }
}

結果を返す

  • setResult(int,Intent) で結果コードと結果インテントを指定する
  • 処理が終了して呼び出し元のアクティビティに戻るときは finish() でアクティビティを閉じる
    • Note: 結果コードはデフォルトで RESULT_CANCELED になっている(ユーザが戻るボタンを押した場合などのため)
// Create intent to deliver some kind of result data
Intent result = new Intent("com.example.RESULT_ACTION", Uri.parse("content://result_uri");
setResult(Activity.RESULT_OK, result);
finish();
  • 結果として整数値を返すだけでよい場合は setResult() に 0 より大きな整数値を渡して呼びだすことも可能
    • 結果の状態がそれほど多くない場合、同じアプリ内でのアクティビティの呼び出しの場合に便利
setResult(RESULT_COLOR_RED);
finish();
  • Note: アクティビティが startActivity で起動されたか startActivityForResult で起動されたかを気にする必要はなく、とりあえず setResult しておけばよい。結果が不要だった場合は無視される
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment