Androidのファイルブラウズを簡単実装

#はじめに
Androidには、標準のファイルブラウズのUIが無かったので、個々に実装する必要があった。
新たなストレージアクセスフレームワークを使用すると、ファイルの読み書きを簡単に実装できたので、覚えとして記す。
GoogleDrive、SDカード、内部共有ストレージにアクセスできる。
サンプルとして作成したのは、テキストファイルをロードして編集し保存するアプリである。

#実装
Activityとして、「Basic Activity」を選択し、MainActivityの中に全てのコードを記述した。プロジェクト名は「FileSample」とした。FloatingButton関係の記述は削除した。

### Javaコード記述


```MainActivity.java
package xx.xx.xx.xx.filesample;

import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.Toast;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;

public class MainActivity extends AppCompatActivity implements View.OnTouchListener {

    static final int REQUEST_OPEN_FILE = 1001;
    static final int REQUEST_CREATE_FILE = 1002;
    static final int REQUEST_DELETE_FILE = 1003;
    enum Mode {OPEN, CREATE, DELETE};
    EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        editText = findViewById(R.id.edit_text);
        editText.setOnTouchListener(this);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        switch (id) {
            case R.id.action_load:
                startFileBrowser(Mode.OPEN);
                return true;
            case R.id.action_save:
                startFileBrowser(Mode.CREATE);
                return true;
            case R.id.action_delete:
                startFileBrowser(Mode.DELETE);
                return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void startFileBrowser(Mode mode) {
        Intent intent = null;
        try {
            switch (mode) {
                case OPEN:
                    intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
                    intent.setType("text/plain");   //TEXT file only
                    intent.addCategory(Intent.CATEGORY_OPENABLE);
                    startActivityForResult(Intent.createChooser(intent, "Open a file"), REQUEST_OPEN_FILE);
                    break;
                case CREATE:
                    intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
                    intent.setType("text/plain");   //TEXT file only
                    intent.putExtra(Intent.EXTRA_TITLE, "newfile.txt");
                    intent.addCategory(Intent.CATEGORY_OPENABLE);
                    startActivityForResult(Intent.createChooser(intent, "Create a file"), REQUEST_CREATE_FILE);
                    break;
                case DELETE:
                    intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
                    intent.setType("text/plain");   //TEXT file only
                    intent.addCategory(Intent.CATEGORY_OPENABLE);
                    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
                    startActivityForResult(Intent.createChooser(intent, "Delete a file"), REQUEST_DELETE_FILE);
                    break;
                default:
            }
        } catch (android.content.ActivityNotFoundException ex) {
            // Potentially direct the user to the Market with a Dialog
            Toast.makeText(this, "Please install a File Browser/Manager", Toast.LENGTH_LONG).show();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        // File load
        if (requestCode == REQUEST_OPEN_FILE) {
            if (resultCode == RESULT_OK && data != null) {
                Uri uri = data.getData();
                if (uri != null) {
                    editText.setText(loadStrFromUri(uri));
                }
            }
        }
        // File save
        else if (requestCode == REQUEST_CREATE_FILE) {
            if (resultCode == RESULT_OK && data != null) {
                Uri uri = data.getData();
                if (uri != null) {
                    saveStrToUri(uri, editText.getText().toString());
                }
            }
        }
        // File delete
        else if (requestCode == REQUEST_DELETE_FILE) {
            if (resultCode == RESULT_OK && data != null) {
                ClipData clipData = data.getClipData();
                if(clipData==null){  // single selection
                    Uri uri = data.getData();
                    deleteUri(uri);
                }else {  // multiple selection
                    for (int i = 0; i < clipData.getItemCount(); i++) {
                        Uri uri = clipData.getItemAt(i).getUri();
                        deleteUri(uri);
                    }
                }
            }
        }
    }

    String loadStrFromUri(Uri uri) {
        String str = "";
        Boolean loop=true;
        try {
            if (uri.getScheme().equals("content")) {
                InputStream iStream = getContentResolver().openInputStream(uri);
                if(iStream==null) loop=false;
                ByteArrayOutputStream bout = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                while (loop) {
                    int len = iStream.read(buffer);
                    if (len < 0) break;
                    bout.write(buffer, 0, len);
                }
                str = bout.toString();
            } else {
                Toast.makeText(this, "Unknown scheme", Toast.LENGTH_LONG).show();
            }
        } catch (Exception e) {
            Toast.makeText(this, "Cannot read the file:" + e.toString(), Toast.LENGTH_LONG).show();
        }
        return str;
    }

    void saveStrToUri(Uri uri, String str) {
        try {
            if (uri.getScheme().equals("content")) {
                OutputStream oStream = getContentResolver().openOutputStream(uri);
                if(oStream!=null) oStream.write(str.getBytes());
            } else {
                Toast.makeText(this, "Unknown scheme", Toast.LENGTH_LONG).show();
            }
        } catch (Exception e) {
            Toast.makeText(this, "Cannot write the file:" + e.toString(), Toast.LENGTH_LONG).show();
        }
    }

    void deleteUri(Uri uri) {
        try {
            DocumentsContract.deleteDocument(getContentResolver(), uri);
        } catch(FileNotFoundException e){
            Toast.makeText(this, "Cannot find the file:" + e.toString(), Toast.LENGTH_LONG).show();
        }
    }

    // 以下は、仮想キーボードをタッチで再表示するためのコードである。
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        view.performClick();
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if(view == editText) showKeyboard();
                break;
        }
        return false;
    }
    void showKeyboard() {
        InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
        if(inputMethodManager != null) inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
    }
}
```
Intent.ACTION_OPEN_DOCUMENTまたはIntent.ACTION_CREATE_DOCUMENTを呼ぶと、ファイルブラウズのUIが出る。そのUIでユーザがテキストファイルを選択すると、対応するURIが返されるので、それを使ってファイルの読み書きを行う。

上記はファイルを選択するUIだが、ディレクトリを選択するUIは、Intent.ACTION_OPEN_DOCUMENT_TREEを使えばよい。

### レイアウトファイル記述

「Basic Activity」では、MainActivity.javaに対応して「activity_main.xml」と「content_main.xml」の2つのレイアウトファイルが作られる。そのうち、content_main.xmlを以下のように記述し、テキストの表示・編集のUIを全画面表示する。

```content_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <EditText
            android:id="@+id/edit_text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:inputType="textMultiLine"
            android:textIsSelectable="true" />
    </ScrollView>

</android.support.constraint.ConstraintLayout>
```
### メニュー記述

メインメニューではファイルのREADとWRITEが選択できる。READを選択するとファイルからテキストを読んで画面に表示する。WRITEを選択すると画面に表示しているテキストをファイルに書く。

```menu_main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="me.osdn.users.watanaby.filesample.MainActivity">
    <item
        android:id="@+id/action_load"
        android:orderInCategory="100"
        android:title="Load"
        app:showAsAction="never" />
    <item
        android:id="@+id/action_save"
        android:orderInCategory="100"
        android:title="Save"
        app:showAsAction="never" />
    <item
        android:id="@+id/action_delete"
        android:orderInCategory="100"
        android:title="Delete"
        app:showAsAction="never" />
</menu>

```

#スクリーンショット
１<img src="https://qiita-image-store.s3.amazonaws.com/0/284841/be58e448-e5b5-d45f-a407-3da0ff887c65.jpeg" width=30%> ２<img src="https://qiita-image-store.s3.amazonaws.com/0/284841/051bb0a9-2fb7-c9f8-f843-01e984129e3c.png" width=30%> ３<img src="https://qiita-image-store.s3.amazonaws.com/0/284841/768552b6-5453-6c55-68bb-f82285b0ce38.png" width=30%>

４<img src="https://qiita-image-store.s3.amazonaws.com/0/284841/cd897a4d-960d-5d03-18c8-6c2b91586bc2.jpeg" width=30%> ５<img src="https://qiita-image-store.s3.amazonaws.com/0/284841/d1638f70-f1ec-6621-8ae1-ed094bfbe9dd.jpeg" width=30%> ６<img src="https://qiita-image-store.s3.amazonaws.com/0/284841/6e46e31c-0a07-5bda-9f75-1120cf783a06.jpeg" width=30%> 

#操作方法
アプリを開始すると編集画面を表示する。
画面右上端のメニューアイコンから、Load、Save、Deleteを呼ぶ（スクリーンショット１）。
ファイルブラウズ画面では、左上端のメニューからストレージを選択できる（スクリーンショット２，３）。
Saveでは、ファイル名をキー入力して新規作成するか、既存ファイル名をタッチして書き換える（スクリーンショット４）。
LoadとDeleteでは、キー入力フィールドは出ない（スクリーンショット５）。
Deleteでは、長押しすると複数ファイルを選択できる(スクリーンショット６）。選択後に画面上部の「開く」をタッチすると削除が実行される。
＊Delete時の「開く」表示は「削除」表示にしたいが、変更方法が分からない。ご存知の方はご教示くだれば幸いです。