あなた:
下記のコードの重複を減らしたい
/** 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
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
// 複数ファイル用
private final ActivityResultLauncher
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
private void figureFilePicker() { launchImageFilePicker(filePickerLauncher); }
private void coverFilePicker() { launchImageFilePicker(filePickerLaunch); }
// 🔹 ファイル選択 UI を開く (SAF) - 単一ファイル限定
private final ActivityResultLauncher
// 🔹 ファイル選択 UI を開く (SAF) - 複数ファイル対応
private final ActivityResultLauncher
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" を設定しておくのもアリ:
→ その後 Java/Kotlin 側で setEnabled(true) すればOKです。 | 方法 | 内容 | 安全性・簡潔さ | |---|---|---| | ✅ 方法1 | Intent.putExtra() でフラグを渡す | ⭐おすすめ(明示的で安全) | | 方法2 | 起動元クラス名から判定 | △ 非推奨(不確実) | | 補足 | XMLで初期無効化 | ◎組み合わせるとベター | 必要なら、Kotlin版や複数ボタン対応コードも書きますよ〜。 ## あなた: mainactivityからボタンの有効化と無効化の切り替え方法 ## ChatGPT: MainActivity から他の Activity に遷移する際に、ボタンを有効化・無効化するには、Intent に情報を渡して、遷移先でボタンの状態を切り替えるのが王道です。
Intent intent = new Intent(MainActivity.this, TargetActivity.class); intent.putExtra("enable_button", true); // ← 有効化したいとき startActivity(intent);
無効化したいときは、false を渡すだけです。
Button cancelButton = findViewById(R.id.button_cancel);
// デフォルトは無効化 boolean enable = getIntent().getBooleanExtra("enable_button", false); cancelButton.setEnabled(enable);
Button cancelButton = findViewById(R.id.button_cancel);
// 有効化 cancelButton.setEnabled(true);
// 無効化 cancelButton.setEnabled(false);
ボタンの状態はどこからでも setEnabled(true/false) で動的に変えられます。
cancelButton.setOnClickListener(v -> { boolean enabled = cancelButton.isEnabled(); cancelButton.setEnabled(!enabled); });
必要に応じて、Kotlin版やフラグメント対応なども出せますので気軽に聞いてください! ## あなた: キャンセルボタンのエラー処理がしたい FATAL EXCEPTION: main Process: io.github.kyukyunyorituryo.aozoraepub3, PID: 10322 java.lang.NullPointerException: Attempt to invoke virtual method 'void io.github.kyukyunyorituryo.aozoraepub3.writer.Epub3Writer.cancel()' on a null object reference buttoncancel.setOnClickListener(v -> cancel()); private void cancel() { epub3Writer.cancel(); epub3ImageWriter.cancel(); aozoraConverter.cancel(); convertCanceled = true; if (webConverter != null) webConverter.canceled(); ## ChatGPT: このクラッシュの原因は、cancel() メソッド内で epub3Writer, epub3ImageWriter, または aozoraConverter のいずれかが null のまま使われていることです。
NullPointerException: Attempt to invoke virtual method 'void io.github.kyukyunyorituryo.aozoraepub3.writer.Epub3Writer.cancel()' on a null object reference
つまり epub3Writer.cancel() の呼び出し時に epub3Writer == null です。
private void cancel() { if (epub3Writer != null) epub3Writer.cancel(); if (epub3ImageWriter != null) epub3ImageWriter.cancel(); if (aozoraConverter != null) aozoraConverter.cancel(); if (webConverter != null) webConverter.canceled();
convertCanceled = true;
}
- すべてのインスタンスが null でないとは限らないので、個別に null チェックを入れるのが安全。 - null だったとしてもエラーを出さずスキップするだけでOKな処理なら、これで十分です。 すべてのインスタンスが null でないとは限らないので、個別に null チェックを入れるのが安全。 null だったとしてもエラーを出さずスキップするだけでOKな処理なら、これで十分です。
Button buttonCancel = findViewById(R.id.button_cancel); if (buttonCancel != null) { buttonCancel.setOnClickListener(v -> cancel()); }
何か初期化のタイミングがずれてる可能性もあるので、「どのタイミングで各インスタンスが初期化されるか」も見直すと安心です。 必要なら、ログ出力で null の原因追跡もお手伝いできます! [blog:g:11696248318754550880:banner]