KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

内部ストレージ内のアプリ固有のディレクトリ内のファイル一覧を選択して、SAFで保存したい

生成したEPUBファイルをSAFでアプリ固有のディレクトリ内から外に保存したいがたまにファイルを見失う。

あなた:

内部ストレージ内のアプリ固有のディレクトリ内のファイル一覧を選択して、SAFで保存したい

ChatGPT:

Androidアプリで内部ストレージのアプリ固有ディレクトリ(Context.getFilesDir()やContext.getExternalFilesDir()など)内のファイルを選択して、SAF(Storage Access Framework)を使って保存するには、以下の2つのステップが必要です。

内部ストレージのアプリ固有ディレクトリには通常アクセス制限がないため、ファイル一覧を取得して、アプリ内で選択 UI を用意できます。

File[] files = context.getFilesDir().listFiles();
for (File file : files) {
    Log.d("MyApp", "ファイル名: " + file.getName());
}
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*"); // 任意のMIMEタイプ
intent.putExtra(Intent.EXTRA_TITLE, "保存ファイル名.txt");
startActivityForResult(intent, CREATE_DOCUMENT_REQUEST_CODE);
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == CREATE_DOCUMENT_REQUEST_CODE && resultCode == RESULT_OK) {
        Uri uri = data.getData();
        if (uri != null) {
            saveFileToUri(uri, selectedFile); // `selectedFile`は選択されたFileオブジェクト
        }
    }
}

private void saveFileToUri(Uri uri, File inputFile) {
    try (InputStream in = new FileInputStream(inputFile);
         OutputStream out = getContentResolver().openOutputStream(uri)) {

        byte[] buffer = new byte[1024];
        int length;
        while ((length = in.read(buffer)) > 0) {
            out.write(buffer, 0, length);
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • SAFではファイルパスは使えません。すべてUriで操作します。
  • Context.getFilesDir()やContext.getExternalFilesDir()の中身は通常他のアプリからは見えません(Scoped Storageの影響外)。
  • ACTION_CREATE_DOCUMENTは保存用、ACTION_OPEN_DOCUMENTは読み込み用です。

SAFではファイルパスは使えません。すべてUriで操作します。

Context.getFilesDir()やContext.getExternalFilesDir()の中身は通常他のアプリからは見えません(Scoped Storageの影響外)。

ACTION_CREATE_DOCUMENTは保存用、ACTION_OPEN_DOCUMENTは読み込み用です。

  1. getFilesDir()などでファイル一覧を取得
  2. アプリ内で選択UIを実装(ListViewやRecyclerViewなど)
  3. Intent.ACTION_CREATE_DOCUMENTで保存先選択
  4. Uriを使って選ばれた場所にファイルをコピー

getFilesDir()などでファイル一覧を取得

アプリ内で選択UIを実装(ListViewやRecyclerViewなど)

Intent.ACTION_CREATE_DOCUMENTで保存先選択

Uriを使って選ばれた場所にファイルをコピー

もっと具体的なサンプル(RecyclerViewやファイル選択含む)も必要なら、気軽に言ってください!

あなた:

実際の完成例を見たい

ChatGPT:

了解です! ここでは、内部ストレージのアプリ固有ディレクトリ内のファイルを一覧表示し、1つ選んでSAFで任意の場所に保存するAndroidアプリの完成例を、最小構成で示します。 (Kotlin & Jetpack推奨なのでKotlinで書きますが、Java版も希望があれば書けます)

package com.example.safexample

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import java.io.*

class MainActivity : AppCompatActivity() {

    private val CREATE_DOCUMENT_REQUEST = 1001
    private var selectedFile: File? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val layout = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
        }

        val listView = ListView(this)
        val files = filesDir.listFiles()
        val fileNames = files.map { it.name }
        val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, fileNames)
        listView.adapter = adapter

        val saveButton = Button(this).apply {
            text = "保存先を選ぶ"
            isEnabled = false
        }

        layout.addView(listView)
        layout.addView(saveButton)
        setContentView(layout)

        listView.setOnItemClickListener { _, _, position, _ ->
            selectedFile = files[position]
            Toast.makeText(this, "${selectedFile?.name} を選択", Toast.LENGTH_SHORT).show()
            saveButton.isEnabled = true
        }

        saveButton.setOnClickListener {
            selectedFile?.let {
                val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
                    addCategory(Intent.CATEGORY_OPENABLE)
                    type = "*/*"
                    putExtra(Intent.EXTRA_TITLE, it.name)
                }
                startActivityForResult(intent, CREATE_DOCUMENT_REQUEST)
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == CREATE_DOCUMENT_REQUEST && resultCode == Activity.RESULT_OK) {
            val uri: Uri? = data?.data
            if (uri != null && selectedFile != null) {
                copyFileToUri(selectedFile!!, uri)
                Toast.makeText(this, "保存しました", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun copyFileToUri(inputFile: File, uri: Uri) {
        try {
            contentResolver.openOutputStream(uri)?.use { out ->
                FileInputStream(inputFile).use { input ->
                    input.copyTo(out)
                }
            }
        } catch (e: IOException) {
            e.printStackTrace()
            Toast.makeText(this, "保存に失敗しました", Toast.LENGTH_SHORT).show()
        }
    }
}

この例だとファイルが存在しないと何も表示されないので、テスト用ファイルをonCreate()の冒頭で1つ作っておきましょう:

val testFile = File(filesDir, "example.txt")
if (!testFile.exists()) {
    testFile.writeText("これはテスト用のファイルです。")
}

SAFは外部ストレージのファイルパスに直接アクセスしないので、特別なパーミッションは不要です。

Javaで同じものを作りたい場合もお手伝いできます。希望があれば声をかけてください!

このコードをビルドして実行すれば、内部ファイルの一覧 → 1つ選択 → SAFで保存先指定 → コピー完了 という流れが確認できます。 さらにUIを洗練させたいとか、複数選択にしたい場合なども対応可能ですよ!

あなた:

Javaで書いて

ChatGPT:

了解! 以下は、内部ストレージのアプリ固有ディレクトリ(getFilesDir())内のファイル一覧を表示し、選択されたファイルをSAFで任意の場所に保存するAndroidアプリのJavaでの完成例です。

package com.example.safexample;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.*;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import java.io.*;

public class MainActivity extends AppCompatActivity {

    private static final int CREATE_DOCUMENT_REQUEST = 1001;
    private File selectedFile = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // テストファイルを作成(なければ)
        File testFile = new File(getFilesDir(), "example.txt");
        if (!testFile.exists()) {
            try (FileWriter writer = new FileWriter(testFile)) {
                writer.write("これはJavaで作成されたテストファイルです。");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        LinearLayout layout = new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);

        ListView listView = new ListView(this);
        File[] files = getFilesDir().listFiles();
        String[] fileNames = new String[files.length];
        for (int i = 0; i < files.length; i++) {
            fileNames[i] = files[i].getName();
        }

        ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
                android.R.layout.simple_list_item_1, fileNames);
        listView.setAdapter(adapter);

        Button saveButton = new Button(this);
        saveButton.setText("保存先を選ぶ");
        saveButton.setEnabled(false);

        layout.addView(listView);
        layout.addView(saveButton);
        setContentView(layout);

        listView.setOnItemClickListener((parent, view, position, id) -> {
            selectedFile = files[position];
            Toast.makeText(this, selectedFile.getName() + " を選択", Toast.LENGTH_SHORT).show();
            saveButton.setEnabled(true);
        });

        saveButton.setOnClickListener(v -> {
            if (selectedFile != null) {
                Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setType("*/*");
                intent.putExtra(Intent.EXTRA_TITLE, selectedFile.getName());
                startActivityForResult(intent, CREATE_DOCUMENT_REQUEST);
            }
        });
    }

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

        if (requestCode == CREATE_DOCUMENT_REQUEST && resultCode == Activity.RESULT_OK) {
            Uri uri = data != null ? data.getData() : null;
            if (uri != null && selectedFile != null) {
                copyFileToUri(selectedFile, uri);
                Toast.makeText(this, "保存しました", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void copyFileToUri(File inputFile, Uri uri) {
        try (
                InputStream in = new FileInputStream(inputFile);
                OutputStream out = getContentResolver().openOutputStream(uri)
        ) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(this, "保存に失敗しました", Toast.LENGTH_SHORT).show();
        }
    }
}
  • テストファイル example.txt を自動生成しているので、最初からリストに何か表示されます。
  • startActivityForResult を使っていますが、API 30+ 以降は ActivityResultLauncher に置き換えることもできます(このコードは互換性重視で旧スタイル)。
  • 権限は不要(SAFはファイルパスに直接触らないため)。

テストファイル example.txt を自動生成しているので、最初からリストに何か表示されます。

startActivityForResult を使っていますが、API 30+ 以降は ActivityResultLauncher に置き換えることもできます(このコードは互換性重視で旧スタイル)。

権限は不要(SAFはファイルパスに直接触らないため)。

  • 複数ファイル選択
  • ファイルタイプ制限(.txtだけなど)
  • RecyclerView対応、モダンUI化

複数ファイル選択

ファイルタイプ制限(.txtだけなど)

RecyclerView対応、モダンUI化

など、どんどんカスタマイズできます!興味があればどこでも深掘りします!

あなた:

UIはactivity_main.xmlに分けて書いて

ChatGPT:

了解です! activity_main.xml にレイアウトを分けた、Java + XML構成での完成例を以下に示します。 ファイル構成は以下の通りになります:

app/
├── java/
│   └── com/example/safexample/
│       └── MainActivity.java
└── res/
    └── layout/
        └── activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <ListView
        android:id="@+id/file_list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <Button
        android:id="@+id/save_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="保存先を選ぶ"
        android:enabled="false" />
</LinearLayout>
package com.example.safexample;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.*;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import java.io.*;

public class MainActivity extends AppCompatActivity {

    private static final int CREATE_DOCUMENT_REQUEST = 1001;
    private File selectedFile = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); // XML レイアウトを読み込み

        ListView listView = findViewById(R.id.file_list);
        Button saveButton = findViewById(R.id.save_button);

        // テストファイルを作成(なければ)
        File testFile = new File(getFilesDir(), "example.txt");
        if (!testFile.exists()) {
            try (FileWriter writer = new FileWriter(testFile)) {
                writer.write("これはJavaで作成されたテストファイルです。");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        File[] files = getFilesDir().listFiles();
        String[] fileNames = new String[files.length];
        for (int i = 0; i < files.length; i++) {
            fileNames[i] = files[i].getName();
        }

        ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
                android.R.layout.simple_list_item_1, fileNames);
        listView.setAdapter(adapter);

        listView.setOnItemClickListener((parent, view, position, id) -> {
            selectedFile = files[position];
            Toast.makeText(this, selectedFile.getName() + " を選択", Toast.LENGTH_SHORT).show();
            saveButton.setEnabled(true);
        });

        saveButton.setOnClickListener(v -> {
            if (selectedFile != null) {
                Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setType("*/*");
                intent.putExtra(Intent.EXTRA_TITLE, selectedFile.getName());
                startActivityForResult(intent, CREATE_DOCUMENT_REQUEST);
            }
        });
    }

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

        if (requestCode == CREATE_DOCUMENT_REQUEST && resultCode == Activity.RESULT_OK) {
            Uri uri = data != null ? data.getData() : null;
            if (uri != null && selectedFile != null) {
                copyFileToUri(selectedFile, uri);
                Toast.makeText(this, "保存しました", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void copyFileToUri(File inputFile, Uri uri) {
        try (
                InputStream in = new FileInputStream(inputFile);
                OutputStream out = getContentResolver().openOutputStream(uri)
        ) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(this, "保存に失敗しました", Toast.LENGTH_SHORT).show();
        }
    }
}
  • アプリを起動すると内部ストレージのファイル一覧が表示されます。
  • ファイルをタップ → 「保存先を選ぶ」ボタンが有効化されます。
  • ボタンを押すと SAF が起動 → ファイルを好きな場所に保存できます。

アプリを起動すると内部ストレージのファイル一覧が表示されます。

ファイルをタップ → 「保存先を選ぶ」ボタンが有効化されます。

ボタンを押すと SAF が起動 → ファイルを好きな場所に保存できます。

必要ならこの上に、ファイルサイズの表示やファイルタイプ制限、モダンな UI(RecyclerView + ViewModel)へのリファクタなども可能です!やってみたい?

あなた:

ファイル一覧を取得するのを関数として分離して

ChatGPT:

もちろんです! ファイル一覧の取得処理を関数として分離すると、コードがスッキリして再利用性も上がります。

次のようにします:

private File[] getInternalFileList() {
    return getFilesDir().listFiles();
}

この関数を使って一覧表示の処理に組み込みます。

以下は activity_main.xml を使ったまま、ファイル取得処理を関数に分離した最新版のコードです:

package com.example.safexample;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.*;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import java.io.*;

public class MainActivity extends AppCompatActivity {

    private static final int CREATE_DOCUMENT_REQUEST = 1001;
    private File selectedFile = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); // XML レイアウトを読み込み

        ListView listView = findViewById(R.id.file_list);
        Button saveButton = findViewById(R.id.save_button);

        createTestFile(); // テストファイル作成

        File[] files = getInternalFileList(); // ← ファイル一覧取得関数を使用
        String[] fileNames = new String[files.length];
        for (int i = 0; i < files.length; i++) {
            fileNames[i] = files[i].getName();
        }

        ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
                android.R.layout.simple_list_item_1, fileNames);
        listView.setAdapter(adapter);

        listView.setOnItemClickListener((parent, view, position, id) -> {
            selectedFile = files[position];
            Toast.makeText(this, selectedFile.getName() + " を選択", Toast.LENGTH_SHORT).show();
            saveButton.setEnabled(true);
        });

        saveButton.setOnClickListener(v -> {
            if (selectedFile != null) {
                Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setType("*/*");
                intent.putExtra(Intent.EXTRA_TITLE, selectedFile.getName());
                startActivityForResult(intent, CREATE_DOCUMENT_REQUEST);
            }
        });
    }

    private void createTestFile() {
        File testFile = new File(getFilesDir(), "example.txt");
        if (!testFile.exists()) {
            try (FileWriter writer = new FileWriter(testFile)) {
                writer.write("これはJavaで作成されたテストファイルです。");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private File[] getInternalFileList() {
        File[] files = getFilesDir().listFiles();
        return files != null ? files : new File[0];
    }

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

        if (requestCode == CREATE_DOCUMENT_REQUEST && resultCode == Activity.RESULT_OK) {
            Uri uri = data != null ? data.getData() : null;
            if (uri != null && selectedFile != null) {
                copyFileToUri(selectedFile, uri);
                Toast.makeText(this, "保存しました", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void copyFileToUri(File inputFile, Uri uri) {
        try (
                InputStream in = new FileInputStream(inputFile);
                OutputStream out = getContentResolver().openOutputStream(uri)
        ) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(this, "保存に失敗しました", Toast.LENGTH_SHORT).show();
        }
    }
}
  • getInternalFileList() 関数を使うことで、将来的にファイルソートやフィルタリングを加えるのも簡単になります。
  • createTestFile() も関数として分離しておくと、テスト時に便利です。

getInternalFileList() 関数を使うことで、将来的にファイルソートやフィルタリングを加えるのも簡単になります。

createTestFile() も関数として分離しておくと、テスト時に便利です。

他にも「拡張子フィルタリングしたい」「ファイルサイズも表示したい」なども追加できます。必要なら教えてください!

あなた:

ボタンを押すとファイル一覧を取得するように変更して

ChatGPT:

了解です! つまり、アプリ起動時にはファイル一覧を表示せず、「ボタンを押したとき」に内部ストレージのアプリ固有ディレクトリ内のファイル一覧を取得して表示するように変更したいということですね。

  • 起動時はファイル一覧を表示しない。
  • ボタン (取得 ボタンなど) を押すとファイル一覧を表示。
  • 「保存先を選ぶ」ボタンは、ファイルを選択するまで非表示 or 無効化。

起動時はファイル一覧を表示しない。

ボタン (取得 ボタンなど) を押すとファイル一覧を表示。

「保存先を選ぶ」ボタンは、ファイルを選択するまで非表示 or 無効化。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <Button
        android:id="@+id/load_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="ファイル一覧を取得" />

    <ListView
        android:id="@+id/file_list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <Button
        android:id="@+id/save_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="保存先を選ぶ"
        android:enabled="false" />
</LinearLayout>
package com.example.safexample;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.*;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import java.io.*;

public class MainActivity extends AppCompatActivity {

    private static final int CREATE_DOCUMENT_REQUEST = 1001;
    private File selectedFile = null;
    private File[] currentFiles;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView listView = findViewById(R.id.file_list);
        Button loadButton = findViewById(R.id.load_button);
        Button saveButton = findViewById(R.id.save_button);

        createTestFile(); // 最初に1つファイルを作っておく(必要なら)

        loadButton.setOnClickListener(v -> {
            currentFiles = getInternalFileList();

            if (currentFiles.length == 0) {
                Toast.makeText(this, "ファイルが見つかりません", Toast.LENGTH_SHORT).show();
                return;
            }

            String[] fileNames = new String[currentFiles.length];
            for (int i = 0; i < currentFiles.length; i++) {
                fileNames[i] = currentFiles[i].getName();
            }

            ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
                    android.R.layout.simple_list_item_1, fileNames);
            listView.setAdapter(adapter);

            Toast.makeText(this, "ファイル一覧を取得しました", Toast.LENGTH_SHORT).show();
        });

        listView.setOnItemClickListener((parent, view, position, id) -> {
            if (currentFiles != null && position < currentFiles.length) {
                selectedFile = currentFiles[position];
                Toast.makeText(this, selectedFile.getName() + " を選択", Toast.LENGTH_SHORT).show();
                saveButton.setEnabled(true);
            }
        });

        saveButton.setOnClickListener(v -> {
            if (selectedFile != null) {
                Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setType("*/*");
                intent.putExtra(Intent.EXTRA_TITLE, selectedFile.getName());
                startActivityForResult(intent, CREATE_DOCUMENT_REQUEST);
            }
        });
    }

    private void createTestFile() {
        File testFile = new File(getFilesDir(), "example.txt");
        if (!testFile.exists()) {
            try (FileWriter writer = new FileWriter(testFile)) {
                writer.write("これはJavaで作成されたテストファイルです。");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private File[] getInternalFileList() {
        File[] files = getFilesDir().listFiles();
        return files != null ? files : new File[0];
    }

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

        if (requestCode == CREATE_DOCUMENT_REQUEST && resultCode == Activity.RESULT_OK) {
            Uri uri = data != null ? data.getData() : null;
            if (uri != null && selectedFile != null) {
                copyFileToUri(selectedFile, uri);
                Toast.makeText(this, "保存しました", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void copyFileToUri(File inputFile, Uri uri) {
        try (
                InputStream in = new FileInputStream(inputFile);
                OutputStream out = getContentResolver().openOutputStream(uri)
        ) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(this, "保存に失敗しました", Toast.LENGTH_SHORT).show();
        }
    }
}
  1. アプリ起動 → 何も表示されない。
  2. 「ファイル一覧を取得」ボタンを押す → ListView にファイル一覧が表示される。
  3. ファイルをタップ → 「保存先を選ぶ」ボタンが有効になる。
  4. 保存ボタンを押すと SAF が起動 → 選んだ場所に保存される。

アプリ起動 → 何も表示されない。

「ファイル一覧を取得」ボタンを押す → ListView にファイル一覧が表示される。

ファイルをタップ → 「保存先を選ぶ」ボタンが有効になる。

保存ボタンを押すと SAF が起動 → 選んだ場所に保存される。

さらにUI改善や、ファイルサイズ/更新日時の表示もできます。必要あれば気軽にどうぞ!

あなた:

取得するファイル一覧の拡張子をepubに限定して

ChatGPT:

了解です! getInternalFileList() 関数で、拡張子が .epub のファイルのみをフィルタリングするように変更すればOKです。

getInternalFileList() を以下のように書き換えます:

private File[] getInternalFileList() {
    File[] allFiles = getFilesDir().listFiles();
    if (allFiles == null) return new File[0];

    return Arrays.stream(allFiles)
            .filter(file -> file.isFile() && file.getName().toLowerCase().endsWith(".epub"))
            .toArray(File[]::new);
}

※ Arrays.stream() を使うには import java.util.Arrays; が必要です。

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.*;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import java.io.*;
import java.util.Arrays;

public class MainActivity extends AppCompatActivity {

    private static final int CREATE_DOCUMENT_REQUEST = 1001;
    private File selectedFile = null;
    private File[] currentFiles;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView listView = findViewById(R.id.file_list);
        Button loadButton = findViewById(R.id.load_button);
        Button saveButton = findViewById(R.id.save_button);

        createTestFile(); // .epub テストファイル作成

        loadButton.setOnClickListener(v -> {
            currentFiles = getInternalFileList();

            if (currentFiles.length == 0) {
                Toast.makeText(this, ".epub ファイルが見つかりません", Toast.LENGTH_SHORT).show();
                listView.setAdapter(null);
                return;
            }

            String[] fileNames = new String[currentFiles.length];
            for (int i = 0; i < currentFiles.length; i++) {
                fileNames[i] = currentFiles[i].getName();
            }

            ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
                    android.R.layout.simple_list_item_1, fileNames);
            listView.setAdapter(adapter);
        });

        listView.setOnItemClickListener((parent, view, position, id) -> {
            if (currentFiles != null && position < currentFiles.length) {
                selectedFile = currentFiles[position];
                Toast.makeText(this, selectedFile.getName() + " を選択", Toast.LENGTH_SHORT).show();
                saveButton.setEnabled(true);
            }
        });

        saveButton.setOnClickListener(v -> {
            if (selectedFile != null) {
                Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setType("*/*");
                intent.putExtra(Intent.EXTRA_TITLE, selectedFile.getName());
                startActivityForResult(intent, CREATE_DOCUMENT_REQUEST);
            }
        });
    }

    private void createTestFile() {
        File testFile = new File(getFilesDir(), "test.epub");
        if (!testFile.exists()) {
            try (FileWriter writer = new FileWriter(testFile)) {
                writer.write("これはテスト用の .epub ファイルです。");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private File[] getInternalFileList() {
        File[] allFiles = getFilesDir().listFiles();
        if (allFiles == null) return new File[0];

        return Arrays.stream(allFiles)
                .filter(file -> file.isFile() && file.getName().toLowerCase().endsWith(".epub"))
                .toArray(File[]::new);
    }

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

        if (requestCode == CREATE_DOCUMENT_REQUEST && resultCode == Activity.RESULT_OK) {
            Uri uri = data != null ? data.getData() : null;
            if (uri != null && selectedFile != null) {
                copyFileToUri(selectedFile, uri);
                Toast.makeText(this, "保存しました", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void copyFileToUri(File inputFile, Uri uri) {
        try (
                InputStream in = new FileInputStream(inputFile);
                OutputStream out = getContentResolver().openOutputStream(uri)
        ) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(this, "保存に失敗しました", Toast.LENGTH_SHORT).show();
        }
    }
}

これで .epub 拡張子だけがリストに表示されるようになります。 他の条件(たとえばファイルサイズで並べ替えるなど)も追加できますので、必要があれば気軽にどうぞ!