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

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

コードのエラー処理など

あなた:

下記のコードの重複を減らしたい

    /** Intent から URL を取得して処理 */
    private void handleIntent(Intent intent) {
        if (intent == null) return;

        String receivedUrl = null;

        // 共有 (SEND) インテントからの取得
        if (Intent.ACTION_SEND.equals(intent.getAction()) && "text/plain".equals(intent.getType())) {
            receivedUrl = intent.getStringExtra(Intent.EXTRA_TEXT);
        }

        // リンクを開く (VIEW) インテントからの取得
        if (Intent.ACTION_VIEW.equals(intent.getAction())) {
            Uri data = intent.getData();
            if (data != null) {
                receivedUrl = data.toString();
            }
        }

        // URL が取得できた場合、変換処理を実行
        if (receivedUrl != null && !receivedUrl.isEmpty()) {
            LogAppender.println("受け取ったURL: " + receivedUrl);

            // URLリストに追加
            List<String> urlList = new ArrayList<>();
            urlList.add(receivedUrl);

            // 出力先フォルダ(キャッシュフォルダを使用)
            File dstPath = getCacheDir();

            // 変換処理を実行
            convertWeb(urlList, new ArrayList<>(), dstPath);
        } else {
            LogAppender.println("URLが空または無効です");
        }
    }
    private void figureFilePicker() {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        String[] mimeTypes ={
                "image/jpeg",
                "image/png"};
        intent.setType("*/*");
        intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
        filePickerLauncher.launch(intent);
    }

    private void coverFilePicker() {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        String[] mimeTypes ={
                "image/jpeg",
                "image/png"};
        intent.setType("*/*").putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
        filePickerLaunch.launch(intent);
    }
    // 🔹 ファイル選択 UI を開く (SAF) - 単一ファイル限定
    private final ActivityResultLauncher<Intent> filePickerLaunch =
            registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
                if (result.getResultCode() == RESULT_OK && result.getData() != null) {
                    Intent data = result.getData();
                    Uri selectedFileUri = data.getData();  // 単一ファイルのみ

                    if (selectedFileUri != null) {
                        copyFileToInternalStorageCover(selectedFileUri);
                    }
                }
            });
    // 🔹 選択したファイルを内部ストレージにコピー
    private void copyFileToInternalStorageCover(Uri uri) {
        File src = null;
        String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME};
        String path = null;
        Cursor cursor = this.getContentResolver().query(uri, projection, null, null, null);
        if (cursor != null) {

            if (cursor.moveToFirst()) {
                path = cursor.getString(0);
            }
            cursor.close();
            Context context = getApplicationContext();
            src = new File(context.getFilesDir(), path);

            System.out.println("filename:" + path);
            System.out.println("filename:" + src.getPath());
        }

        try {
            Files.copy(getContentResolver().openInputStream(uri), src.toPath(), StandardCopyOption.REPLACE_EXISTING);
            Toast.makeText(this, "ファイルを内部ストレージにコピーしました", Toast.LENGTH_SHORT).show();
            coverFileName=src.getPath();
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(this, "コピーに失敗しました", Toast.LENGTH_SHORT).show();
        }
    }
    // 🔹 ファイル選択 UI を開く (SAF) - 複数ファイル対応
    private final ActivityResultLauncher<Intent> filePickerLauncher =
            registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
                if (result.getResultCode() == RESULT_OK && result.getData() != null) {
                    Intent data = result.getData();
                    List<Uri> uriList = new ArrayList<>();

                    // 複数ファイル選択
                    if (data.getClipData() != null) {
                        ClipData clipData = data.getClipData();
                        for (int i = 0; i < clipData.getItemCount(); i++) {
                            uriList.add(clipData.getItemAt(i).getUri());
                        }
                    }
                    // 単一ファイル選択
                    else if (data.getData() != null) {
                        uriList.add(data.getData());
                    }

                    // 選択されたすべてのファイルを処理
                    for (Uri uri : uriList) {
                        copyFileToInternalStorage(uri);
                    }
                }
            });
``
## ChatGPT:
以下のように、コードの重複部分をメソッド化することで、重複を減らし、保守性を高めることができます。主に以下の点を整理・共通化しました:

1. Intent作成処理(figureFilePicker / coverFilePicker の共通部分)
2. Intent から URL を抽出する処理(handleIntent内の共通化)
3. copyFileToInternalStorage系メソッドの共通化(処理が類似している)

Intent作成処理(figureFilePicker / coverFilePicker の共通部分)

Intent から URL を抽出する処理(handleIntent内の共通化)

copyFileToInternalStorage系メソッドの共通化(処理が類似している)

/* Intent から URL を取得して処理 / private void handleIntent(Intent intent) { if (intent == null) return;

String receivedUrl = extractUrlFromIntent(intent);

if (receivedUrl != null && !receivedUrl.isEmpty()) {
    LogAppender.println("受け取ったURL: " + receivedUrl);

    List<String> urlList = new ArrayList<>();
    urlList.add(receivedUrl);
    File dstPath = getCacheDir();

    convertWeb(urlList, new ArrayList<>(), dstPath);
} else {
    LogAppender.println("URLが空または無効です");
}

}

/* URL抽出を共通化 / private String extractUrlFromIntent(Intent intent) { String action = intent.getAction(); String type = intent.getType();

if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
    return intent.getStringExtra(Intent.EXTRA_TEXT);
} else if (Intent.ACTION_VIEW.equals(action)) {
    Uri data = intent.getData();
    return data != null ? data.toString() : null;
}
return null;

}




private void launchImageFilePicker(ActivityResultLauncher launcher) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("/"); intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/jpeg", "image/png"}); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); launcher.launch(intent); }

private void figureFilePicker() { launchImageFilePicker(filePickerLauncher); }

private void coverFilePicker() { launchImageFilePicker(filePickerLaunch); }




private void copyFileToInternalStorage(Uri uri, boolean isCover) { String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME}; String path = null;

try (Cursor cursor = getContentResolver().query(uri, projection, null, null, null)) {
    if (cursor != null && cursor.moveToFirst()) {
        path = cursor.getString(0);
    }
}

if (path == null) return;

File dstFile = new File(getFilesDir(), path);
try {
    Files.copy(getContentResolver().openInputStream(uri), dstFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
    Toast.makeText(this, "ファイルを内部ストレージにコピーしました", Toast.LENGTH_SHORT).show();

    if (isCover) {
        coverFileName = dstFile.getPath();
    }
} catch (IOException e) {
    e.printStackTrace();
    Toast.makeText(this, "コピーに失敗しました", Toast.LENGTH_SHORT).show();
}

}




// coverFile用 private final ActivityResultLauncher filePickerLaunch = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK && result.getData() != null) { Uri uri = result.getData().getData(); if (uri != null) { copyFileToInternalStorage(uri, true); } } } );

// 複数ファイル用 private final ActivityResultLauncher filePickerLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK && result.getData() != null) { List uriList = new ArrayList<>(); Intent data = result.getData();

        if (data.getClipData() != null) {
            for (int i = 0; i < data.getClipData().getItemCount(); i++) {
                uriList.add(data.getClipData().getItemAt(i).getUri());
            }
        } else if (data.getData() != null) {
            uriList.add(data.getData());
        }

        for (Uri uri : uriList) {
            copyFileToInternalStorage(uri, false);
        }
    }
}

);

必要に応じて、copyFileToInternalStorage(uri)(元々の関数名)を残して、copyFileToInternalStorageCoverなどと切り分けても良いですが、boolean引数で処理を分けるのがより汎用的です。

もっとまとめたい部分があれば教えてください!また、リファクタ後の全体コードが欲しければまとめて書き出します。

## あなた:
そのまま置き換えれるような全体のコードが見たい

## ChatGPT:
了解です!では、あなたの元のコードを重複を減らすようにリファクタリングして、そのまま置き換え可能な全体コードを提示します。

/* Intent から URL を取得して処理 / private void handleIntent(Intent intent) { if (intent == null) return;

String receivedUrl = extractUrlFromIntent(intent);

if (receivedUrl != null && !receivedUrl.isEmpty()) {
    LogAppender.println("受け取ったURL: " + receivedUrl);

    List<String> urlList = new ArrayList<>();
    urlList.add(receivedUrl);
    File dstPath = getCacheDir();

    convertWeb(urlList, new ArrayList<>(), dstPath);
} else {
    LogAppender.println("URLが空または無効です");
}

}

/* URL抽出を共通化 / private String extractUrlFromIntent(Intent intent) { String action = intent.getAction(); String type = intent.getType();

if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
    return intent.getStringExtra(Intent.EXTRA_TEXT);
} else if (Intent.ACTION_VIEW.equals(action)) {
    Uri data = intent.getData();
    return data != null ? data.toString() : null;
}
return null;

}

/ 共通の画像ファイルピッカーを起動 / private void launchImageFilePicker(ActivityResultLauncher launcher) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("/*"); intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/jpeg", "image/png"}); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); launcher.launch(intent); }

private void figureFilePicker() { launchImageFilePicker(filePickerLauncher); }

private void coverFilePicker() { launchImageFilePicker(filePickerLaunch); }

// 🔹 ファイル選択 UI を開く (SAF) - 単一ファイル限定 private final ActivityResultLauncher filePickerLaunch = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK && result.getData() != null) { Uri uri = result.getData().getData(); // 単一ファイルのみ if (uri != null) { copyFileToInternalStorage(uri, true); } } });

// 🔹 ファイル選択 UI を開く (SAF) - 複数ファイル対応 private final ActivityResultLauncher filePickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK && result.getData() != null) { Intent data = result.getData(); List uriList = new ArrayList<>();

            if (data.getClipData() != null) {
                ClipData clipData = data.getClipData();
                for (int i = 0; i < clipData.getItemCount(); i++) {
                    uriList.add(clipData.getItemAt(i).getUri());
                }
            } else if (data.getData() != null) {
                uriList.add(data.getData());
            }

            for (Uri uri : uriList) {
                copyFileToInternalStorage(uri, false);
            }
        }
    });

// 🔹 選択したファイルを内部ストレージにコピー private void copyFileToInternalStorage(Uri uri, boolean isCover) { String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME}; String fileName = null;

try (Cursor cursor = getContentResolver().query(uri, projection, null, null, null)) {
    if (cursor != null && cursor.moveToFirst()) {
        fileName = cursor.getString(0);
    }
}

if (fileName == null) return;

File dstFile = new File(getFilesDir(), fileName);
try {
    Files.copy(getContentResolver().openInputStream(uri), dstFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
    Toast.makeText(this, "ファイルを内部ストレージにコピーしました", Toast.LENGTH_SHORT).show();

    if (isCover) {
        coverFileName = dstFile.getPath();
    }
} catch (IOException e) {
    e.printStackTrace();
    Toast.makeText(this, "コピーに失敗しました", Toast.LENGTH_SHORT).show();
}

}

| 元の重複コード | 変更後の処理 |
|---|---|
| Intentでのファイル選択処理 | launchImageFilePicker(...) で共通化 |
| Intent からの URL 抽出 | extractUrlFromIntent(...) に分離 |
| copyFileToInternalStorage 系 | copyFileToInternalStorage(uri, isCover) に一本化し、カバー用は true |

このまま既存コードに置き換えれば、動作は変えずに重複が減り、メンテナンス性が上がります。

他にもリファクタしたい箇所があれば教えてください!

## あなた:
 processFile()の後にopenFileSaver();を実行したい

     buttonProcess.setOnClickListener(v -> 
                processFile():
        );

## ChatGPT:
processFile() のあとに openFileSaver() を呼びたい場合は、ラムダ式内で processFile() の後に openFileSaver() を順番に書けばOKです。以下のように書きます:

buttonProcess.setOnClickListener(v -> { processFile(); openFileSaver(); });

このように {} で囲むことで、複数行の処理をラムダ式に記述できます。
(今のように1行だけなら {} は省略できますが、複数処理を入れるなら必須です)

もし processFile() が非同期処理(例えば AsyncTask や Handler、Coroutine など)で、完了してから openFileSaver() を呼びたい場合は、その完了を検知してから呼ぶ必要があります。そういう場合は教えてくれたら、それに合ったやり方を提案できます!

今は「順番に実行すればOK」なケースとして書いてます。

## あなた:
エラーコードの説明をして

FATAL EXCEPTION: main
                                                                                                    Process: io.github.kyukyunyorituryo.aozoraepub3, PID: 9442
                                                                                                    java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.io.File.exists()' on a null object reference

## ChatGPT:
このエラーメッセージは、Java(またはAndroid)でよくある「NullPointerException(ヌルポインタ例外)」です。

java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.io.File.exists()' on a null object reference

null のオブジェクトに対して .exists() メソッドを呼び出そうとしたため、クラッシュした。

File file = null; のように file が null のまま、

file.exists(); // ← ここでクラッシュ

と書いている、という状態です。

たとえば次のようなコードがあったとします:

File file = getSomeFile(); // 何らかのメソッドで取得 if (file.exists()) { // 処理 }

この場合、getSomeFile() が null を返している可能性があります。

1. null チェックを追加する:

null チェックを追加する:

if (file != null && file.exists()) { // 安全に使える }

1. file が null になっている原因を追跡する:

File file = new File(path); の path が null や空ではないか?

URI からパスを変換する部分で失敗していないか?

getFilesDir() や getExternalFilesDir() を使う時に context が null じゃないか?
2. File file = new File(path); の path が null や空ではないか?
3. URI からパスを変換する部分で失敗していないか?
4. getFilesDir() や getExternalFilesDir() を使う時に context が null じゃないか?

file が null になっている原因を追跡する:

- File file = new File(path); の path が null や空ではないか?
- URI からパスを変換する部分で失敗していないか?
- getFilesDir() や getExternalFilesDir() を使う時に context が null じゃないか?

File file = new File(path); の path が null や空ではないか?

URI からパスを変換する部分で失敗していないか?

getFilesDir() や getExternalFilesDir() を使う時に context が null じゃないか?

クラッシュ時のログ(Logcat)でどの行で例外が起きたか(YourClassName.java:123 など)も書かれているので、それを参考にしましょう。

もし copyFileToInternalStorage(...) や openFileSaver() 周辺で File file = null; がありそうなら、その辺りが原因の可能性もあります。

コードを貼ってもらえたら、どこが null かピンポイントで指摘できます!

## あなた:
エラー処理の問題を修正して

        if (!srcFile.exists() || srcFile.length() == 0) {
            Toast.makeText(this, "入力ファイルが存在しないか空です", Toast.LENGTH_SHORT).show();
            return;
        }

## ChatGPT:
このコードで NullPointerException が出ている原因は、srcFile が null の可能性があるのに srcFile.exists() を呼んでいることです。
なので、まず srcFile != null をチェックするようにしましょう。

if (srcFile == null || !srcFile.exists() || srcFile.length() == 0) { Toast.makeText(this, "入力ファイルが存在しないか空です", Toast.LENGTH_SHORT).show(); return; }

- ||(または)を使って、最初に null を確認すれば、以降のメソッド呼び出しでクラッシュしません。
- null チェック → 存在チェック → サイズチェック の順が安全です。

||(または)を使って、最初に null を確認すれば、以降のメソッド呼び出しでクラッシュしません。

null チェック → 存在チェック → サイズチェック の順が安全です。

さらに安全にしたいならログ出力を追加して、どの条件で引っかかったのか確認するのも手です:

if (srcFile == null) { Log.e("FileCheck", "srcFile is null"); Toast.makeText(this, "入力ファイルが指定されていません", Toast.LENGTH_SHORT).show(); return; } if (!srcFile.exists()) { Log.e("FileCheck", "srcFile does not exist: " + srcFile.getPath()); Toast.makeText(this, "入力ファイルが存在しません", Toast.LENGTH_SHORT).show(); return; } if (srcFile.length() == 0) { Log.e("FileCheck", "srcFile is empty: " + srcFile.getPath()); Toast.makeText(this, "入力ファイルが空です", Toast.LENGTH_SHORT).show(); return; }

必要に応じてこのバージョンも使ってください!

## あなた:
下記のコードのエラー処理をして

 FATAL EXCEPTION: main
                                                                                                    Process: io.github.kyukyunyorituryo.aozoraepub3, PID: 9815
                                                                                                    java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.io.File.getName()' on a null object reference

    private void openFileSaver() {
        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("application/epub+zip");
        intent.putExtra(Intent.EXTRA_TITLE, outFile.getName());
        saveFileLauncher.launch(intent);
    }
    

## ChatGPT:
このエラーの原因は、outFile が null の状態で outFile.getName() を呼び出しているためです。

NullPointerException: Attempt to invoke virtual method 'java.lang.String java.io.File.getName()' on a null object reference




intent.putExtra(Intent.EXTRA_TITLE, outFile.getName());

このとき outFile == null なのでクラッシュ。

private void openFileSaver() { if (outFile == null) { Toast.makeText(this, "出力ファイルが指定されていません", Toast.LENGTH_SHORT).show(); Log.e("openFileSaver", "outFile is null"); return; }

Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/epub+zip");
intent.putExtra(Intent.EXTRA_TITLE, outFile.getName());
saveFileLauncher.launch(intent);

}

万が一 outFile が null のときでも、とりあえずファイル保存ダイアログを出したい場合:

private void openFileSaver() { String fileName = "output.epub"; if (outFile != null) { fileName = outFile.getName(); } else { Log.w("openFileSaver", "outFile is null, using default file name"); }

Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/epub+zip");
intent.putExtra(Intent.EXTRA_TITLE, fileName);
saveFileLauncher.launch(intent);

}

どちらにするかはアプリの設計次第だけど、
「outFile が null なら保存しない」という方が安全設計になります!

## あなた:
mainactivityから実行時のみボタンを有効化し、それ以外は無効化したい
    <Button
        android:id="@+id/button_cancel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="キャンセル" />

## ChatGPT:
MainActivity から起動したときだけボタンを有効化し、それ以外(他の Activity から遷移してきた場合など)は無効化したい場合、起動元を判別する方法が必要です。

Intent intent = new Intent(this, YourTargetActivity.class); intent.putExtra("launched_from_main", true); startActivity(intent);




Button cancelButton = findViewById(R.id.button_cancel); boolean isFromMain = getIntent().getBooleanExtra("launched_from_main", false); cancelButton.setEnabled(isFromMain);




Button cancelButton = findViewById(R.id.button_cancel); String callingActivity = getCallingActivity() != null ? getCallingActivity().getClassName() : null; if (callingActivity != null && callingActivity.contains("MainActivity")) { cancelButton.setEnabled(true); } else { cancelButton.setEnabled(false); }

⚠️ ただし getCallingActivity() は startActivityForResult() で起動したときのみ有効なので、通常は 方法1(Intentにフラグを入れる) の方がおすすめです。

「MainActivity 以外から来たときは基本無効」にするなら、XML側で enabled="false" を設定しておくのもアリ: