Android: SDカード上のファイルへの読み書き

- Android Studio 4.0.1
- Kotlin 1.4.0
- Minimum SDK: API 23

AndroidアプリでSDカード上にファイルを書き込み、またそれを読みだす手順をまとめます。サンドボックスの中に作成する場合はもう少し簡単です。共有フォルダなどに保存して、パソコンなどからもアクセスできるようにすることが目的の方法です。

## 準備

### Manifestへ権限追加

※権限の追加は不要なようです。
※権限の確認は実行時に行われます。

```xml:AndroidManifest.xml
<!-- 必要ない -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```

### DocumentFileクラス取り込み

**Intent.ACTION_OPEN_DOCUMENT_TREE**でのアクセスで使用します。
**Intent.ACTION_CREATE_DOCUMENT**と**Intent.ACTION_OPEN_DOCUMENT**で一つづつアクセスする場合はいりいません。

```gradle:build.gradle(Module:app)
dependencies {
    implementation "androidx.documentfile:documentfile:1.0.1"
}
```

## 権限の許可を取得

ユーザーに権限の確認を求めます。
※この処理は、必要ないそうです。が、備忘のため、いちおう残しておきます。

```kotlin:MainActivity
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)

		ActivityCompat.requestPermissions(this, arrayOf(
				Manifest.permission.WRITE_EXTERNAL_STORAGE
		), 1)
	}

	override fun onRequestPermissionsResult(
			requestCode: Int,
			permissions: Array<out String>,
			grantResults: IntArray
	) {
		if (requestCode == 1) {
			var allowed = true
			for (result in grantResults) {
				if (result == PackageManager.PERMISSION_DENIED) {
					allowed = false
				}
			}
			if (!allowed) {
				finish()
			}
		}
	}
}
```

## ファイルを書き込む、読み込む

### テキストファイルを出力

出力するフォルダ、ファイル名をユーザーに選択させます。
アクセス権限がないフォルダを選択した場合などは、失敗します。

```kotlin:MainActivity
	fun openWriteFile() {
		val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
		intent.setType("text/plain")
		intent.putExtra(Intent.EXTRA_TITLE, "sample1.txt")
		startActivityForResult(intent, 101);
	}

	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
		if (requestCode == 101 && resultCode == RESULT_OK) {
			if (data?.getData() != null) {
				val uri: Uri = data.getData() as Uri
				val out = contentResolver.openOutputStream(uri)
				val writer = OutputStreamWriter(out)
				writer.write("サンプルテキスト\n2行目")
				writer.close()
			}
		}
		super.onActivityResult(requestCode, resultCode, data)
	}

```

読み込むファイルをユーザーに選択させます。

```kotlin:MainActivity
	fun openReadFile() {
		val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
		intent.setType("text/plain")
		startActivityForResult(intent, 102);
	}

	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
		if (requestCode == 102 && resultCode == RESULT_OK) {
			if (data?.getData() != null) {
				val uri: Uri = data.getData() as Uri
				val input = contentResolver.openInputStream(uri)
				val reader = InputStreamReader(input)
				Log.d("nozaki", reader.readText())
			}
		}
		super.onActivityResult(requestCode, resultCode, data)
	}

```


## フォルダ単位でアクセスする

基準となるフォルダをユーザーに選択させます。
そこにサブフォルダを作り、サブフォルダにファイルを作成します。

```kotlin:MainActivity
	fun openFolder() {
		val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
		startActivityForResult(intent, 103);
	}

	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
		if (requestCode == 103 && resultCode == RESULT_OK) {
			if (data?.getData() != null) {
				// ファイルアクセスのためのオブジェクトを取得する
				val treeUri: Uri = data.getData() as Uri
				val document = DocumentFile.fromTreeUri(this, treeUri)

				// 作成するフォルダが既にあれば、そのオブジェクトを取得、なければ作成
				var dir = document?.findFile("SampleFolder")
				if (dir == null) {
					dir = document?.createDirectory("SampleFolder")
				}
				if (dir == null || !dir.isDirectory()) {
					return
				}

				// フォルダにファイルを作成する
				val file = dir.createFile("text/plain", "sample2.txt")
				if (file != null) {
					val out = contentResolver.openOutputStream(file.uri)
					val writer = OutputStreamWriter(out)
					writer.write("サブフォルダへのサンプルテキスト\n2行目")
					writer.close()
				}
			}
		}
		super.onActivityResult(requestCode, resultCode, data)
	}
```

ユーザーが選択したフォルダのファイル一覧を取得します。

```kotlin:MainActivity

	fun openFolder() {
		val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
		startActivityForResult(intent, 104);
	}

	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
		if (requestCode == 104 && resultCode == RESULT_OK) {
			if (data?.getData() != null) {
				// ファイルアクセスのためのオブジェクトを取得する
				val treeUri: Uri = data.getData() as Uri
				val document = DocumentFile.fromTreeUri(this, treeUri)
				if (document == null) {
					return
				}

				// フォルダのファイル一覧をログに出力する
				for (file in document.listFiles()) {
					Log.d("nozaki", file.name)
				}
			}
		}
		super.onActivityResult(requestCode, resultCode, data)
	}
```

## アクセス権限の保存

一度ユーザーにフォルダを選択させ、その権限を保存しておくと、次回からはユーザーの確認を待つことなくそのフォルダにアクセスできるようになります。

### 権限と対象フォルダのUriを保存

```kotlin:MainActivity
getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val pref = getSharedPreferences("app", Context.MODE_PRIVATE)
pref.edit().putString("treeUri", treeUri.toString()).commit()
```

### 保存したフォルダからファイル一覧を取得

ユーザーへの確認は行われません

```kotlin:MainActivity
	fun readFolder() {
		val pref = getSharedPreferences("app", Context.MODE_PRIVATE)
		val treeUri = Uri.parse(pref.getString("treeUri", null))
		val document = DocumentFile.fromTreeUri(this, treeUri)
		if (document == null) {
			return
		}
		for (file in document.listFiles()) {
			Log.d("nozaki", file.name)
		}
	}
```