あなた:
下記のコードの重複を減らしたい
/** 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]