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

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

ListPreferenceから選択後に編集可能にしたい

あなた:

listから選択後に編集可能にしたい <ListPreference android:key="UserAgent" android:title="ユーザーエージェント" android:summary="ユーザーエージェントを設定します" android:entries="@array/ua_entries" android:entryValues="@array/ua_values" android:defaultValue="" />

ChatGPT:

ListPreference で リストから選択後にカスタム入力を許可(編集可能)にするには、標準の ListPreference では直接対応できません。 ただし、以下のような方法で実現可能です👇

  1. ListPreference に「カスタム入力」という選択肢を追加する。
  2. 「カスタム入力」が選ばれたときだけ EditTextPreference を表示・有効化する。

ListPreference に「カスタム入力」という選択肢を追加する。

「カスタム入力」が選ばれたときだけ EditTextPreference を表示・有効化する。

<PreferenceCategory android:title="ユーザーエージェント設定">

    <ListPreference
        android:key="UserAgentType"
        android:title="ユーザーエージェント"
        android:summary="ユーザーエージェントを設定します"
        android:entries="@array/ua_entries"
        android:entryValues="@array/ua_values"
        android:defaultValue="default" />

    <EditTextPreference
        android:key="UserAgentCustom"
        android:title="カスタムユーザーエージェント"
        android:summary="カスタムUAを入力"
        android:dialogTitle="ユーザーエージェントを入力してください"
        android:enabled="false"
        android:defaultValue="" />
</PreferenceCategory>
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
    setPreferencesFromResource(R.xml.your_pref_file, rootKey)

    val listPref = findPreference<ListPreference>("UserAgentType")
    val editPref = findPreference<EditTextPreference>("UserAgentCustom")

    listPref?.setOnPreferenceChangeListener { _, newValue ->
        val selected = newValue as String
        val isCustom = selected == "custom" // entryValues で "custom" を指定していると仮定
        editPref?.isEnabled = isCustom
        true
    }

    // 初期表示時に状態を反映
    editPref?.isEnabled = listPref?.value == "custom"
}
<string-array name="ua_entries">
    <item>標準</item>
    <item>モバイル</item>
    <item>カスタム</item>
</string-array>
<string-array name="ua_values">
    <item>default</item>
    <item>mobile</item>
    <item>custom</item>
</string-array>
  • 標準 や モバイル を選んだ場合はカスタム入力は無効。
  • カスタム を選ぶと、下に現れる EditTextPreference で編集可能に。

標準 や モバイル を選んだ場合はカスタム入力は無効。

カスタム を選ぶと、下に現れる EditTextPreference で編集可能に。

これが一番簡単で保守性のある方法です。 カスタム DialogPreference を作って完全に一つにまとめることもできますが、それは少し複雑になります。 必要ならその実装もお手伝いできます!

あなた:

javaで書いて

ChatGPT:

もちろん、PreferenceFragmentCompat を Java で書いた例を以下に示します。ListPreference で「カスタム」を選んだときにだけ、EditTextPreference を有効化するコードです。

<PreferenceCategory android:title="ユーザーエージェント設定">
    <ListPreference
        android:key="UserAgentType"
        android:title="ユーザーエージェント"
        android:summary="ユーザーエージェントを設定します"
        android:entries="@array/ua_entries"
        android:entryValues="@array/ua_values"
        android:defaultValue="default" />

    <EditTextPreference
        android:key="UserAgentCustom"
        android:title="カスタムユーザーエージェント"
        android:summary="カスタムUAを入力"
        android:dialogTitle="ユーザーエージェントを入力してください"
        android:enabled="false"
        android:defaultValue="" />
</PreferenceCategory>
public class SettingsFragment extends PreferenceFragmentCompat {

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        setPreferencesFromResource(R.xml.preferences, rootKey);

        ListPreference listPref = findPreference("UserAgentType");
        EditTextPreference editPref = findPreference("UserAgentCustom");

        if (listPref != null && editPref != null) {
            // 初期状態の有効/無効を設定
            editPref.setEnabled("custom".equals(listPref.getValue()));

            listPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
                @Override
                public boolean onPreferenceChange(Preference preference, Object newValue) {
                    String selected = (String) newValue;
                    boolean isCustom = "custom".equals(selected);
                    editPref.setEnabled(isCustom);
                    return true;
                }
            });
        }
    }
}
<string-array name="ua_entries">
    <item>標準</item>
    <item>モバイル</item>
    <item>カスタム</item>
</string-array>
<string-array name="ua_values">
    <item>default</item>
    <item>mobile</item>
    <item>custom</item>
</string-array>

この構成で、ListPreference から「カスタム」を選択すると EditTextPreference が編集可能になります。それ以外を選ぶと非活性になります。

もっと一体化させたUIを作りたい場合はカスタム DialogPreference を使う方法もあります。必要ならそれも紹介しますね。

あなた:

次のpreferenceから設定値を取得する方法。Javaで変数に代入して

<!-- 出力先 -->
<PreferenceCategory app:title="出力先">

    <SwitchPreferenceCompat
        app:key="same_path"
        app:title="入力と同じ場所に出力"
        android:defaultValue="true" />

    <EditTextPreference
        app:key="output_path"
        app:title="出力先フォルダ"
        app:summary="出力先を指定します"
        app:dialogTitle="フォルダパスを入力"
        app:enabled="false" />
</PreferenceCategory>

<!-- 変換 -->
<PreferenceCategory app:title="変換設定">

    <ListPreference
        app:key="input_encoding"
        app:title="入力文字コード"
        app:entries="@array/input_encoding_entries"
        app:entryValues="@array/input_encoding_values"
        android:defaultValue="AUTO" />

    <ListPreference
        app:key="language"
        app:title="言語"
        app:entries="@array/language_entries"
        app:entryValues="@array/language_values"
        android:defaultValue="ja" />

    <ListPreference
        app:key="text_direction"
        app:title="縦書き/横書き"
        app:entries="@array/text_direction_entries"
        app:entryValues="@array/text_direction_values"
        android:defaultValue="vertical" />

    <Preference
        app:key="select_file"
        app:title="ファイル選択"
        app:summary="ファイルを選択して変換を開始"
        />
</PreferenceCategory>

<!-- 挿絵除外 -->
<CheckBoxPreference
    app:key="exclude_illustrations"
    app:title="挿絵除外"
    app:summary="テキストの画像注記は表紙と外字画像以外はePubに格納されません"
    android:defaultValue="false" />

<!-- 画面サイズ -->
<PreferenceCategory app:title="画面・表紙サイズ">

    <EditTextPreference
        app:key="screen_width"
        app:title="画面 横(px)"
        android:inputType="number"
        android:defaultValue="1600" />

    <EditTextPreference
        app:key="screen_height"
        app:title="画面 縦(px)"
        android:inputType="number"
        android:defaultValue="2560" />

    <EditTextPreference
        app:key="cover_width"
        app:title="表紙 横(px)"
        android:inputType="number"
        android:defaultValue="0" />

    <EditTextPreference
        app:key="cover_height"
        app:title="表紙 縦(px)"
        android:inputType="number"
        android:defaultValue="0" />

</PreferenceCategory>

<!-- 画像倍率 -->
<PreferenceCategory app:title="画像表示倍率">

    <CheckBoxPreference
        app:key="image_scale_enabled"
        app:title="有効"
        app:summary="画面の解像度に合わせて画像の幅を%指定します"
        android:defaultValue="true" />

    <EditTextPreference
        app:key="image_scale_factor"
        app:title="画像表示倍率(倍)"
        android:inputType="numberDecimal"
        android:defaultValue="1.0" />
</PreferenceCategory>

ChatGPT:

AndroidのPreference画面から設定値をJavaで取得するには、SharedPreferencesを使用します。以下のようにして変数に代入できます。

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

// 出力先
boolean samePath = prefs.getBoolean("same_path", true);
String outputPath = prefs.getString("output_path", "");

// 変換設定
String inputEncoding = prefs.getString("input_encoding", "AUTO");
String language = prefs.getString("language", "ja");
String textDirection = prefs.getString("text_direction", "vertical");

// 挿絵除外
boolean excludeIllustrations = prefs.getBoolean("exclude_illustrations", false);

// 画面・表紙サイズ
int screenWidth = Integer.parseInt(prefs.getString("screen_width", "1600"));
int screenHeight = Integer.parseInt(prefs.getString("screen_height", "2560"));
int coverWidth = Integer.parseInt(prefs.getString("cover_width", "0"));
int coverHeight = Integer.parseInt(prefs.getString("cover_height", "0"));

// 画像倍率
boolean imageScaleEnabled = prefs.getBoolean("image_scale_enabled", true);
float imageScaleFactor = Float.parseFloat(prefs.getString("image_scale_factor", "1.0"));
  • EditTextPreference や ListPreference は 常にgetString()で取得される ため、数値として使う場合は Integer.parseInt() や Float.parseFloat() を使ってください。
  • SwitchPreferenceCompat や CheckBoxPreference は getBoolean() で取得します。
  • Preference(ファイル選択のやつ)には値は保存されないので、SharedPreferences で取得する必要はありません。

EditTextPreference や ListPreference は 常にgetString()で取得される ため、数値として使う場合は Integer.parseInt() や Float.parseFloat() を使ってください。

SwitchPreferenceCompat や CheckBoxPreference は getBoolean() で取得します。

Preference(ファイル選択のやつ)には値は保存されないので、SharedPreferences で取得する必要はありません。

PreferenceFragmentCompat などの中で使う場合は requireContext() や getContext() を使えばOKです。アクティビティなら this で。

必要なら、PreferenceFragmentCompat内での使い方や、設定の更新に応じてリアルタイムで変数を更新する方法も説明できます!

あなた:

次のpreferenceから設定値を取得する方法。Javaで変数に代入して

<!-- 画像回り込み -->
<PreferenceCategory app:title="画像回り込み (※単ページ化より優先)">
    <SwitchPreferenceCompat
        app:key="image_float_enabled"
        app:title="回り込み有効"
        app:summary="画像の実サイズが指定サイズ以下の画像を回り込み設定します"
        android:defaultValue="false" />

    <EditTextPreference
        app:key="image_float_width"
        app:title="横 (px)"
        app:dialogTitle="画像回り込み 横サイズ"
        android:inputType="number"
        android:defaultValue="600" />

    <EditTextPreference
        app:key="image_float_height"
        app:title="縦 (px)"
        app:dialogTitle="画像回り込み 縦サイズ"
        android:inputType="number"
        android:defaultValue="400" />

    <ListPreference
        app:key="image_float_position"
        app:title="配置"
        android:defaultValue="上/左"
        app:entries="@array/image_float_position_entries"
        app:entryValues="@array/image_float_position_values" />
</PreferenceCategory>

<!-- 画像単ページ化 -->
<PreferenceCategory app:title="画像単ページ化">
    <EditTextPreference
        app:key="single_page_width"
        app:title="単ページ 横 (px)"
        android:inputType="number"
        android:defaultValue="200" />

    <EditTextPreference
        app:key="single_page_height"
        app:title="単ページ 縦 (px)"
        android:inputType="number"
        android:defaultValue="300" />

    <EditTextPreference
        app:key="single_page_width_only"
        app:title="横のみ (px)"
        android:inputType="number"
        android:defaultValue="300" />

    <ListPreference
        app:key="image_size_mode"
        app:title="縮小表示モード"
        android:defaultValue="none"
        app:entries="@array/image_size_mode_entries"
        app:entryValues="@array/image_size_mode_values" />

    <SwitchPreferenceCompat
        app:key="fit_image"
        app:title="拡大表示"
        app:summary="画面サイズより小さい画像を幅高さに合わせて拡大表示します"
        android:defaultValue="true" />
</PreferenceCategory>

<!-- Float指定 -->
<PreferenceCategory app:title="Float指定 (Readerのみ)">
    <CheckBoxPreference
        app:key="float_image_single_page"
        app:title="単ページ画像"
        app:summary="単ページ対象の画像をfloat表示します。xhtmlは分割されません" />
    <CheckBoxPreference
        app:key="float_image_block"
        app:title="通常画像"
        app:summary="回り込み、単ページ以外の画像をfloat表示します。64px以上の画像のみ" />
</PreferenceCategory>

<!-- 全画面表示 -->
<PreferenceCategory app:title="全画面表示">
    <CheckBoxPreference
        app:key="output_svg"
        app:title="SVGタグ出力(画像zipのみ)"
        app:summary="画像のみのzipの場合、固定レイアウト+SVGタグで出力します" />
</PreferenceCategory>

<!-- Jpeg圧縮率 -->
<PreferenceCategory app:title="Jpeg圧縮率">
    <EditTextPreference
        app:key="jpeg_quality"
        app:title="圧縮率 (30~100)"
        app:summary="表紙編集、縮小、回転、余白除去時のJpeg保存時の画質(100が最高画質)"
        android:inputType="number" />
</PreferenceCategory>

<!-- 色調整 -->
<PreferenceCategory app:title="色調整">
    <CheckBoxPreference
        app:key="gamma_correction"
        app:title="ガンマ補正"
        app:summary="画像の濃さを変更します (濃:0.2~1.8:淡)" />
    <EditTextPreference
        app:key="gamma_value"
        app:title="ガンマ値"
        app:summary="例: 1.0"
        android:inputType="numberDecimal" />
</PreferenceCategory>

ChatGPT:

以下は、上記のPreferenceからJavaコードで設定値を取得し、変数に代入する例です。SharedPreferences を使ってすべての値を読み取ります。

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

// 画像回り込み
boolean imageFloatEnabled = prefs.getBoolean("image_float_enabled", false);
int imageFloatWidth = Integer.parseInt(prefs.getString("image_float_width", "600"));
int imageFloatHeight = Integer.parseInt(prefs.getString("image_float_height", "400"));
String imageFloatPosition = prefs.getString("image_float_position", "上/左");

// 画像単ページ化
int singlePageWidth = Integer.parseInt(prefs.getString("single_page_width", "200"));
int singlePageHeight = Integer.parseInt(prefs.getString("single_page_height", "300"));
int singlePageWidthOnly = Integer.parseInt(prefs.getString("single_page_width_only", "300"));
String imageSizeMode = prefs.getString("image_size_mode", "none");
boolean fitImage = prefs.getBoolean("fit_image", true);

// Float指定 (Readerのみ)
boolean floatImageSinglePage = prefs.getBoolean("float_image_single_page", false);
boolean floatImageBlock = prefs.getBoolean("float_image_block", false);

// 全画面表示
boolean outputSvg = prefs.getBoolean("output_svg", false);

// Jpeg圧縮率
int jpegQuality = Integer.parseInt(prefs.getString("jpeg_quality", "100")); // default: 100

// 色調整
boolean gammaCorrection = prefs.getBoolean("gamma_correction", false);
float gammaValue = Float.parseFloat(prefs.getString("gamma_value", "1.0")); // default: 1.0
  • EditTextPreference → getString() で取得 → int や float にパース。
  • SwitchPreferenceCompat, CheckBoxPreference → getBoolean()。
  • ListPreference → getString()(選択されたvalueが入ってます)。
  • context は Activity なら this、PreferenceFragmentCompat なら requireContext()。

EditTextPreference → getString() で取得 → int や float にパース。

SwitchPreferenceCompat, CheckBoxPreference → getBoolean()。

ListPreference → getString()(選択されたvalueが入ってます)。

context は Activity なら this、PreferenceFragmentCompat なら requireContext()。

他にも特定の値の妥当性チェックや、null対策・try-catch を入れたい場合は、その処理も書けるので気軽に聞いてください!

あなた:

次のpreferenceから設定値を取得する方法。Javaで変数に代入して

<!-- 表題 -->

<PreferenceCategory android:title="余白除去">

    <SwitchPreferenceCompat
        android:key="auto_margin_enabled"
        android:title="有効"
        android:defaultValue="false" />

    <EditTextPreference
        android:key="auto_margin_limit_h"
        android:title="横方向の余白除去量 (%)"
        android:defaultValue="15"
        android:enabled="false"
        android:inputType="number" />

    <EditTextPreference
        android:key="auto_margin_limit_v"
        android:title="縦方向の余白除去量 (%)"
        android:defaultValue="15"
        android:enabled="false"
        android:inputType="number" />

    <EditTextPreference
        android:key="auto_margin_padding"
        android:title="余白追加量 (%)"
        android:defaultValue="1.0"
        android:enabled="false"
        android:inputType="numberDecimal" />

    <EditTextPreference
        android:key="auto_margin_white_level"
        android:title="白レベル (0〜100)"
        android:defaultValue="80"
        android:enabled="false"
        android:inputType="number" />

    <ListPreference
        android:key="auto_margin_nombre_position"
        android:title="ノンブル除去位置"
        android:defaultValue="none"
        android:entries="@array/nombre_position_entries"
        android:entryValues="@array/nombre_position_values" />

    <EditTextPreference
        android:key="auto_margin_nombre_size"
        android:title="ノンブル高さ (%)"
        android:defaultValue="3.0"
        android:enabled="false"
        android:inputType="numberDecimal" />

</PreferenceCategory>

<!-- 文中全角スペースの処理 -->
<PreferenceCategory android:title="文中全角スペースの処理">
    <ListPreference
        android:key="space_handling"
        android:title="行末で非表示"
        android:entries="@array/space_handling_entries"
        android:entryValues="@array/space_handling_values"
        android:defaultValue="default"
        android:summary="%s" />
</PreferenceCategory>

<!-- 「○○」に「××」の注記 -->
<PreferenceCategory android:title="「○○」に「××」の注記">
    <ListPreference
        android:key="annotation_handling"
        android:title="注記の表示方法"
        android:entries="@array/annotation_entries"
        android:entryValues="@array/annotation_values"
        android:defaultValue="0"
        android:summary="%s" />
</PreferenceCategory>

<!-- 自動縦中横 -->
<PreferenceCategory android:title="自動縦中横">
    <SwitchPreferenceCompat
        android:key="enable_auto_yoko"
        android:title="有効"
        android:defaultValue="true"
        android:summary="2文字の数字、!?を縦中横で表示します。" />
    <CheckBoxPreference
        android:key="auto_yoko_num1"
        android:title="1桁数字を縦中横"
        android:defaultValue="false" />
    <CheckBoxPreference
        android:key="auto_yoko_num3"
        android:title="3桁数字を縦中横"
        android:defaultValue="false" />
    <CheckBoxPreference
        android:key="auto_eq1"
        android:title="!? 1文字"
        android:defaultValue="false" />
</PreferenceCategory>
<!-- コメントブロック出力 -->
<PreferenceCategory android:title="コメントブロック出力">
    <CheckBoxPreference
        android:key="comment_output"
        android:title="コメント出力"
        android:summary="コメント行の間を出力します"
        android:defaultValue="false" />

    <CheckBoxPreference
        android:key="comment_convert"
        android:title="コメント内注記変換"
        android:summary="コメント内の注記を変換します"
        android:defaultValue="false" />
</PreferenceCategory>

ChatGPT:

以下は、上記のPreference XMLから設定値をJavaで取得して変数に代入するコード例です。すべて SharedPreferences を使って取得します。

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

// --- 余白除去 ---
boolean autoMarginEnabled = prefs.getBoolean("auto_margin_enabled", false);
int autoMarginLimitH = Integer.parseInt(prefs.getString("auto_margin_limit_h", "15"));
int autoMarginLimitV = Integer.parseInt(prefs.getString("auto_margin_limit_v", "15"));
float autoMarginPadding = Float.parseFloat(prefs.getString("auto_margin_padding", "1.0"));
int autoMarginWhiteLevel = Integer.parseInt(prefs.getString("auto_margin_white_level", "80"));
String autoMarginNombrePosition = prefs.getString("auto_margin_nombre_position", "none");
float autoMarginNombreSize = Float.parseFloat(prefs.getString("auto_margin_nombre_size", "3.0"));

// --- 文中全角スペースの処理 ---
String spaceHandling = prefs.getString("space_handling", "default");

// --- 「○○」に「××」の注記 ---
String annotationHandling = prefs.getString("annotation_handling", "0");

// --- 自動縦中横 ---
boolean enableAutoYoko = prefs.getBoolean("enable_auto_yoko", true);
boolean autoYokoNum1 = prefs.getBoolean("auto_yoko_num1", false);
boolean autoYokoNum3 = prefs.getBoolean("auto_yoko_num3", false);
boolean autoEq1 = prefs.getBoolean("auto_eq1", false);

// --- コメントブロック出力 ---
boolean commentOutput = prefs.getBoolean("comment_output", false);
boolean commentConvert = prefs.getBoolean("comment_convert", false);
  • EditTextPreference からは文字列として取得 → 数値変換(Integer.parseInt() や Float.parseFloat())が必要です。
  • SwitchPreferenceCompat、CheckBoxPreference は getBoolean() で取得。
  • ListPreference は選択された entryValue が String で保存されるので getString() で取得。

EditTextPreference からは文字列として取得 → 数値変換(Integer.parseInt() や Float.parseFloat())が必要です。

SwitchPreferenceCompat、CheckBoxPreference は getBoolean() で取得。

ListPreference は選択された entryValue が String で保存されるので getString() で取得。

他にも「数値の範囲チェックを入れたい」「UIが変更されたらリアルタイムで読み取りたい」などのニーズがあれば、それも対応できます!

あなた:

次のpreferenceから設定値を取得する方法。Javaで変数に代入して

<!-- 栞用ID -->
<PreferenceCategory android:title="栞用ID">
    <CheckBoxPreference
        android:key="mark_id"
        android:title="各行に出力"
        android:summary="Kobo向けの栞を記憶するためのIDを各行に設定します"
        android:defaultValue="false" />
</PreferenceCategory>

<!-- 空行除去 -->
<PreferenceCategory android:title="空行除去">
    <ListPreference
        android:key="remove_empty_line"
        android:title="空行を減らす"
        android:summary="空行の行数を減らします。見出し行の後ろ3行以内は1行残します"
        android:entries="@array/remove_empty_line_entries"
        android:entryValues="@array/remove_empty_line_values"
        android:defaultValue="0" />

    <ListPreference
        android:key="max_empty_line"
        android:title="最大空行数"
        android:summary="空行の連続を指定行数以下に制限します"
        android:entries="@array/max_empty_line_entries"
        android:entryValues="@array/max_empty_line_values"
        android:defaultValue="-" />
</PreferenceCategory>

<!-- 行頭字下げ -->
<PreferenceCategory android:title="行頭字下げ">
    <CheckBoxPreference
        android:key="force_indent"
        android:title="字下げ有効"
        android:summary="行頭が「『―”(〈〔【と全角空白以外なら行頭に全角空白を追加します。半角空白のみは全角に置き換えます"
        android:defaultValue="false" />
</PreferenceCategory>

<!-- 強制改ページ -->
<PreferenceCategory android:title="強制改ページ">

    <!-- 強制改ページ有効 -->
    <CheckBoxPreference
        android:key="page_break_enabled"
        android:title="有効"
        android:summary="指定サイズを超えた時点で強制改ページ(ブロック注記の外側のみ)"
        android:defaultValue="true" />

    <!-- 改ページサイズ(KB) -->
    <EditTextPreference
        android:key="page_break_size_kb"
        android:title="改ページサイズ"
        android:summary="改ページ判定に使う本文サイズ(KB)"
        android:defaultValue="400"
        android:inputType="number" />

    <!-- 空行による改ページの有効化 -->
    <CheckBoxPreference
        android:key="page_break_empty_enabled"
        android:title="空行による改ページ"
        android:summary="指定行数以上の空行で改ページ(サイズ制限付き)"
        android:defaultValue="false" />

    <!-- 空行行数 -->
    <ListPreference
        android:key="page_break_empty_line_count"
        android:title="空行行数"
        android:summary="空行の行数(1〜9)"
        android:entries="@array/page_break_empty_line_entries"
        android:entryValues="@array/page_break_empty_line_values"
        android:defaultValue="2" />

    <!-- 空行サイズ制限 -->
    <EditTextPreference
        android:key="page_break_empty_size_kb"
        android:title="空行による改ページのサイズ制限(KB)"
        android:summary="空行による改ページが発動する本文サイズ"
        android:defaultValue="300"
        android:inputType="number" />

    <!-- 見出し前の改ページ -->
    <CheckBoxPreference
        android:key="page_break_chapter_enabled"
        android:title="見出し前で改ページ"
        android:summary="見出し前で改ページを入れる(サイズ制限付き)"
        android:defaultValue="false" />

    <!-- 見出し前の改ページサイズ -->
    <EditTextPreference
        android:key="page_break_chapter_size_kb"
        android:title="見出し前の改ページサイズ(KB)"
        android:summary="見出し前で改ページする際の本文サイズ制限"
        android:defaultValue="200"
        android:inputType="number" />

</PreferenceCategory>

ChatGPT:

以下は、指定された Preference XML に対応する Java コードで設定値を取得して変数に代入 する例です。すべて SharedPreferences を使って取得します。

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

// --- 栞用ID ---
boolean markId = prefs.getBoolean("mark_id", false);

// --- 空行除去 ---
String removeEmptyLine = prefs.getString("remove_empty_line", "0");
String maxEmptyLine = prefs.getString("max_empty_line", "-");

// --- 行頭字下げ ---
boolean forceIndent = prefs.getBoolean("force_indent", false);

// --- 強制改ページ ---
boolean pageBreakEnabled = prefs.getBoolean("page_break_enabled", true);
int pageBreakSizeKb = Integer.parseInt(prefs.getString("page_break_size_kb", "400"));

boolean pageBreakEmptyEnabled = prefs.getBoolean("page_break_empty_enabled", false);
String pageBreakEmptyLineCount = prefs.getString("page_break_empty_line_count", "2");
int pageBreakEmptySizeKb = Integer.parseInt(prefs.getString("page_break_empty_size_kb", "300"));

boolean pageBreakChapterEnabled = prefs.getBoolean("page_break_chapter_enabled", false);
int pageBreakChapterSizeKb = Integer.parseInt(prefs.getString("page_break_chapter_size_kb", "200"));
  • EditTextPreference → getString() で取得してから Integer.parseInt() などで数値化。
  • CheckBoxPreference → getBoolean()。
  • ListPreference → 選ばれた entryValue を getString() で取得。

EditTextPreference → getString() で取得してから Integer.parseInt() などで数値化。

CheckBoxPreference → getBoolean()。

ListPreference → 選ばれた entryValue を getString() で取得。

さらに「nullや例外を防ぎたい」「Optional設定がある」など詳細なニーズがあれば、それも考慮したコードにできますよ!

あなた:

次のpreferenceから設定値を取得する方法。Javaで変数に代入して

<!-- 目次設定 -->
<PreferenceCategory android:title="目次設定">

    <!-- 最大文字数 -->
    <EditTextPreference
        android:key="max_chapter_name_length"
        android:title="最大文字数"
        android:summary="見出しの最大文字数"
        android:defaultValue="64"
        android:inputType="number" />

    <!-- 表紙を目次に追加 -->
    <CheckBoxPreference
        android:key="toc_include_cover"
        android:title="表紙"
        android:summary="表紙画像のページを目次に追加します"
        android:defaultValue="false" />

    <!-- 表題を目次に追加 -->
    <CheckBoxPreference
        android:key="toc_include_title"
        android:title="表題"
        android:summary="表題の行を目次に追加します"
        android:defaultValue="true" />

    <!-- 次の行を繋げる -->
    <CheckBoxPreference
        android:key="toc_join_next_line"
        android:title="次の行を繋げる"
        android:summary="次の行が空行でなければ見出しの後ろに繋げます"
        android:defaultValue="false" />

    <!-- 連続する見出しを除外 -->
    <CheckBoxPreference
        android:key="toc_exclude_repeated"
        android:title="連続する見出しを除外"
        android:summary="3つ以上連続する自動抽出された見出しを除外します(空行1行間隔も連続扱い)"
        android:defaultValue="true" />

    <!-- nav階層化 -->
    <CheckBoxPreference
        android:key="nav_nested"
        android:title="目次ページ階層化"
        android:summary="目次ページ(nav.xhtml)を階層化します"
        android:defaultValue="true" />

    <!-- ncx階層化 -->
    <CheckBoxPreference
        android:key="ncx_nested"
        android:title="目次(ncx)階層化"
        android:summary="目次(toc.ncx)を階層化します"
        android:defaultValue="true" />

</PreferenceCategory>

<!-- 目次抽出 -->
<PreferenceCategory android:title="目次抽出">

    <!-- 見出し注記 -->
    <CheckBoxPreference
        android:key="chapter_head"
        android:title="見出し"
        android:defaultValue="true" />

    <CheckBoxPreference
        android:key="chapter_head1"
        android:title="大見出し"
        android:defaultValue="true" />

    <CheckBoxPreference
        android:key="chapter_head2"
        android:title="中見出し"
        android:defaultValue="true" />

    <CheckBoxPreference
        android:key="chapter_head3"
        android:title="小見出し"
        android:defaultValue="true" />

    <CheckBoxPreference
        android:key="chapter_same_line"
        android:title="同行見出し含む"
        android:defaultValue="false" />

    <!-- 改ページ後 -->
    <CheckBoxPreference
        android:key="chapter_after_pagebreak"
        android:title="改ページ後"
        android:summary="改ページ後の先頭行の文字を目次に出力します"
        android:defaultValue="true" />

    <!-- 章見出し -->
    <CheckBoxPreference
        android:key="chapter_name"
        android:title="章見出し (第~章/その~/~章/序/プロローグ 等)"
        android:summary="第~話/第~章/第~篇/第~部/第~節/第~幕/第~編/その~/~章/プロローグ/エピローグ/モノローグ/序/序章/終章/転章/間章/幕間"
        android:defaultValue="true" />

ChatGPT:

以下は、提示された 「目次設定」「目次抽出」 の Preference から Javaで設定値を取得して変数に代入するコード例 です。

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

// --- 目次設定 ---
int maxChapterNameLength = Integer.parseInt(prefs.getString("max_chapter_name_length", "64"));

boolean tocIncludeCover = prefs.getBoolean("toc_include_cover", false);
boolean tocIncludeTitle = prefs.getBoolean("toc_include_title", true);
boolean tocJoinNextLine = prefs.getBoolean("toc_join_next_line", false);
boolean tocExcludeRepeated = prefs.getBoolean("toc_exclude_repeated", true);
boolean navNested = prefs.getBoolean("nav_nested", true);
boolean ncxNested = prefs.getBoolean("ncx_nested", true);

// --- 目次抽出 ---
boolean chapterHead = prefs.getBoolean("chapter_head", true);
boolean chapterHead1 = prefs.getBoolean("chapter_head1", true);
boolean chapterHead2 = prefs.getBoolean("chapter_head2", true);
boolean chapterHead3 = prefs.getBoolean("chapter_head3", true);
boolean chapterSameLine = prefs.getBoolean("chapter_same_line", false);
boolean chapterAfterPagebreak = prefs.getBoolean("chapter_after_pagebreak", true);
boolean chapterName = prefs.getBoolean("chapter_name", true);
  • EditTextPreference の値は 文字列として取得し、数値に変換(Integer.parseInt()など) します。
  • CheckBoxPreference はそのまま getBoolean()。
  • ListPreference(今回なし)の場合は getString() で選択値を取得。

EditTextPreference の値は 文字列として取得し、数値に変換(Integer.parseInt()など) します。

CheckBoxPreference はそのまま getBoolean()。

ListPreference(今回なし)の場合は getString() で選択値を取得。

もし SharedPreferences を別の名前で使っている場合(例:getSharedPreferences("settings", MODE_PRIVATE))、それに置き換えてくださいね。

他にも「nullのときはこうしたい」とか「まとめてクラスにしたい」などの希望があれば、そこも対応できます!

あなた:

次のpreferenceから設定値を取得する方法。Javaで変数に代入して

    <!-- 数字系 -->
    <CheckBoxPreference
        android:key="chapter_num_only"
        android:title="数字のみ"
        android:defaultValue="false" />

    <CheckBoxPreference
        android:key="chapter_num_title"
        android:title="数字+見出し"
        android:defaultValue="false" />

    <CheckBoxPreference
        android:key="chapter_num_paren"
        android:title="括弧内数字のみ"
        android:summary="()〈〉〔〕【】内の数字"
        android:defaultValue="false" />

    <CheckBoxPreference
        android:key="chapter_num_paren_title"
        android:title="括弧内数字+見出し"
        android:defaultValue="false" />

    <!-- その他パターン -->
    <CheckBoxPreference
        android:key="chapter_pattern_enabled"
        android:title="その他パターン"
        android:summary="目次抽出パターンを正規表現で指定します。前後の空白とタグを除いた文字列と比較します。"
        android:defaultValue="false" />

    <ListPreference
        android:key="chapter_pattern"
        android:title="パターン選択"
        android:entries="@array/chapter_patterns_entries"
        android:entryValues="@array/chapter_patterns_values"
        android:defaultValue="^(見出し1|見出し2|見出し3)$"
        android:enabled="true" />

</PreferenceCategory>
<!-- スタイル -->
<PreferenceCategory android:title="スタイル">

    <!-- 行の高さ -->
    <EditTextPreference
        android:key="line_height"
        android:title="行の高さ"
        android:dialogTitle="行の高さを入力"
        android:inputType="numberDecimal"
        android:defaultValue="1.8"
        android:summary="1.3 ~ 2.0" />

    <!-- 文字サイズ -->
    <EditTextPreference
        android:key="font_size"
        android:title="文字サイズ (%)"
        android:dialogTitle="文字サイズを入力"
        android:inputType="number"
        android:defaultValue="100"
        android:summary="75 ~ 125%" />

</PreferenceCategory>

<!-- 太字ゴシック表示 -->
<PreferenceCategory android:title="太字ゴシック表示">
    <CheckBoxPreference
        android:key="bold_note"
        android:title="太字注記"
        android:summary="太字注記を太字ゴシックで表示"
        android:defaultValue="false" />
    <CheckBoxPreference
        android:key="gothic_note"
        android:title="ゴシック体注記"
        android:summary="ゴシック体注記を太字ゴシックで表示"
        android:defaultValue="false" />
</PreferenceCategory>

<!-- テキスト余白 -->
<PreferenceCategory android:title="テキスト余白 (@page margin)">
    <EditTextPreference
        android:key="page_margin_top"
        android:title="上"
        android:inputType="numberDecimal"
        android:defaultValue="0.5" />
    <EditTextPreference
        android:key="page_margin_right"
        android:title="右"
        android:inputType="numberDecimal"
        android:defaultValue="0.5" />
    <EditTextPreference
        android:key="page_margin_bottom"
        android:title="下"
        android:inputType="numberDecimal"
        android:defaultValue="0.5" />
    <EditTextPreference
        android:key="page_margin_left"
        android:title="左"
        android:inputType="numberDecimal"
        android:defaultValue="0.5" />

    <!-- 単位選択 -->
    <ListPreference
        android:key="page_margin_unit"
        android:title="単位"
        android:entries="@array/margin_units"
        android:entryValues="@array/margin_units_values"
        android:defaultValue="char" />
</PreferenceCategory>

ChatGPT:

以下は、あなたが提示した「数字系」「スタイル」「太字ゴシック表示」「テキスト余白」の Preference から、Javaで設定値を変数に代入する方法です。

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

// --- 数字系 ---
boolean chapterNumOnly = prefs.getBoolean("chapter_num_only", false);
boolean chapterNumTitle = prefs.getBoolean("chapter_num_title", false);
boolean chapterNumParen = prefs.getBoolean("chapter_num_paren", false);
boolean chapterNumParenTitle = prefs.getBoolean("chapter_num_paren_title", false);
boolean chapterPatternEnabled = prefs.getBoolean("chapter_pattern_enabled", false);
String chapterPattern = prefs.getString("chapter_pattern", "^(見出し1|見出し2|見出し3)$");

// --- スタイル ---
float lineHeight = Float.parseFloat(prefs.getString("line_height", "1.8"));
int fontSize = Integer.parseInt(prefs.getString("font_size", "100"));

// --- 太字ゴシック表示 ---
boolean boldNote = prefs.getBoolean("bold_note", false);
boolean gothicNote = prefs.getBoolean("gothic_note", false);

// --- テキスト余白 ---
float pageMarginTop = Float.parseFloat(prefs.getString("page_margin_top", "0.5"));
float pageMarginRight = Float.parseFloat(prefs.getString("page_margin_right", "0.5"));
float pageMarginBottom = Float.parseFloat(prefs.getString("page_margin_bottom", "0.5"));
float pageMarginLeft = Float.parseFloat(prefs.getString("page_margin_left", "0.5"));
String pageMarginUnit = prefs.getString("page_margin_unit", "char");
  • EditTextPreference の値は getString() で取り出し、必要に応じて Float.parseFloat() や Integer.parseInt() で変換。
  • ListPreference も基本は getString()。
  • CheckBoxPreference は getBoolean() でOK。
  • 数値は入力チェック(例:try-catch)があると安全。

EditTextPreference の値は getString() で取り出し、必要に応じて Float.parseFloat() や Integer.parseInt() で変換。

ListPreference も基本は getString()。

CheckBoxPreference は getBoolean() でOK。

数値は入力チェック(例:try-catch)があると安全。

必要であれば、上記の値をまとめて保持する 設定クラス を作ることもできます。ご希望があれば続けて作成しますよ!

あなた:

次のpreferenceから設定値を取得する方法。Javaで変数に代入して

<!-- テキスト余白 (html margin) Reader用 -->
<PreferenceCategory android:title="テキスト余白 (html margin) Reader用">
    <EditTextPreference
        android:key="body_margin_top"
        android:title="上"
        android:inputType="numberDecimal"
        android:defaultValue="0" />
    <EditTextPreference
        android:key="body_margin_right"
        android:title="右"
        android:inputType="numberDecimal"
        android:defaultValue="0" />
    <EditTextPreference
        android:key="body_margin_bottom"
        android:title="下"
        android:inputType="numberDecimal"
        android:defaultValue="0" />
    <EditTextPreference
        android:key="body_margin_left"
        android:title="左"
        android:inputType="numberDecimal"
        android:defaultValue="0" />

    <!-- 単位選択 -->
    <ListPreference
        android:key="body_margin_unit"
        android:title="単位"
        android:entries="@array/margin_units"
        android:entryValues="@array/margin_units_values"
        android:defaultValue="char" />
</PreferenceCategory>
<!-- 文字出力 -->
<PreferenceCategory android:title="文字出力">

    <!-- 濁点/半濁点文字 -->
    <ListPreference
        android:key="dakuten_output_type"
        android:title="濁点/半濁点文字の出力"
        android:dialogTitle="濁点/半濁点文字の出力方法を選択"
        android:entries="@array/dakuten_output_options"
        android:entryValues="@array/dakuten_output_values"
        android:defaultValue="font"
        android:summary="濁点/半濁点文字の出力方法を選択します" />

</PreferenceCategory>

<!-- IVS出力 -->
<PreferenceCategory android:title="IVS出力 (Kobo, Kindle非対応)">
    <CheckBoxPreference
        android:key="ivs_bmp"
        android:title="英数字用 (U+FE00-FE0E)"
        android:summary="英数字、絵文字向けのIVSを出力"
        android:defaultValue="false" />
    <CheckBoxPreference
        android:key="ivs_ssp"
        android:title="漢字用 (U+E0100-E01EF)"
        android:summary="漢字用のIVSを出力"
        android:defaultValue="false" />
</PreferenceCategory>
<!-- Web -->
<PreferenceCategory android:title="Web">
</PreferenceCategory>

<PreferenceCategory android:title="取得設定">
    <EditTextPreference
        android:key="web_interval"
        android:title="取得間隔"
        android:summary="Web小説の取得間隔(秒)を設定します"
        android:defaultValue="0.5"
        android:inputType="numberDecimal" />
</PreferenceCategory>

<PreferenceCategory android:title="UA設定">
    <ListPreference
        android:key="ua_type"
        android:title="ユーザーエージェント"
        android:summary="ユーザーエージェントを設定します"
        android:entries="@array/ua_entries"
        android:entryValues="@array/ua_values"
        android:defaultValue="" />
</PreferenceCategory>

<PreferenceCategory android:title="Web画像設定">
    <CheckBoxPreference
        android:key="web_large_image"
        android:title="画像大"
        android:summary="Web画像のオリジナルを取得" />
</PreferenceCategory>

<PreferenceCategory android:title="キャッシュ保存パス">
    <EditTextPreference
        android:key="cache_path"
        android:title="保存パス"
        android:summary="キャッシュファイルを保存するパスです。フルパスまたは相対パスを指定"
        android:defaultValue=".cache" />
</PreferenceCategory>

<PreferenceCategory android:title="更新判定">
    <EditTextPreference
        android:key="web_modified_expire"
        android:title="更新間隔"
        android:summary="この時間以内に取得したキャッシュを更新分として処理します"
        android:defaultValue="24"
        android:inputType="number" />
</PreferenceCategory>

ChatGPT:

以下は、提示していただいた設定群から Javaで設定値を変数に代入する方法 のサンプルコードです。すべて SharedPreferences 経由で取得し、数値は必要に応じて Float.parseFloat() や Integer.parseInt() を使って変換します。

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

// --- テキスト余白 (html margin) Reader用 ---
float bodyMarginTop = Float.parseFloat(prefs.getString("body_margin_top", "0"));
float bodyMarginRight = Float.parseFloat(prefs.getString("body_margin_right", "0"));
float bodyMarginBottom = Float.parseFloat(prefs.getString("body_margin_bottom", "0"));
float bodyMarginLeft = Float.parseFloat(prefs.getString("body_margin_left", "0"));
String bodyMarginUnit = prefs.getString("body_margin_unit", "char");

// --- 文字出力 ---
String dakutenOutputType = prefs.getString("dakuten_output_type", "font");

// --- IVS出力 ---
boolean ivsBmp = prefs.getBoolean("ivs_bmp", false);
boolean ivsSsp = prefs.getBoolean("ivs_ssp", false);

// --- Web取得設定 ---
float webInterval = Float.parseFloat(prefs.getString("web_interval", "0.5"));

// --- UA設定 ---
String uaType = prefs.getString("ua_type", "");

// --- Web画像設定 ---
boolean webLargeImage = prefs.getBoolean("web_large_image", false);

// --- キャッシュ保存パス ---
String cachePath = prefs.getString("cache_path", ".cache");

// --- 更新判定 ---
int webModifiedExpire = Integer.parseInt(prefs.getString("web_modified_expire", "24"));
  • EditTextPreference → 常に prefs.getString(...) を使って取得 → Float.parseFloat() や Integer.parseInt() で変換。
  • CheckBoxPreference → prefs.getBoolean(...)
  • ListPreference → prefs.getString(...) で選択値をそのまま取得。
  • defaultValue に指定されている値と同じ型でデフォルトを入れると安心。

EditTextPreference → 常に prefs.getString(...) を使って取得 → Float.parseFloat() や Integer.parseInt() で変換。

CheckBoxPreference → prefs.getBoolean(...)

ListPreference → prefs.getString(...) で選択値をそのまま取得。

defaultValue に指定されている値と同じ型でデフォルトを入れると安心。

必要であれば、これらをまとめた Config クラスのようなものを作って管理することもできます!必要でしたらその例も出しますよ!

あなた:

次のpreferenceから設定値を取得する方法。Javaで変数に代入して

<PreferenceCategory android:title="ePub出力設定">
    <CheckBoxPreference
        android:key="web_convert_updated_only"
        android:title="更新時のみ出力"
        android:summary="新規追加または更新がある場合のみePubを出力します" />
</PreferenceCategory>

<PreferenceCategory android:title="変換対象">
    <CheckBoxPreference
        android:key="web_latest_only"
        android:title="最新"
        android:summary="最新話から指定話数のみ出力します" />

    <EditTextPreference
        android:key="web_latest_count"
        android:title="出力話数"
        android:summary="最新話からの出力数"
        android:defaultValue="1"
        android:inputType="number" />

    <CheckBoxPreference
        android:key="web_modified_only"
        android:title="更新分"
        android:summary="追加更新のあった話のみ変換します" />

    <CheckBoxPreference
        android:key="web_modified_tail_only"
        android:title="連続"
        android:summary="最新話から連続した更新分のみ変換します" />
</PreferenceCategory>

ChatGPT:

以下は、指定された <PreferenceScreen> に含まれる設定値を Javaで取得して変数に代入する方法 のコード例です。すべて SharedPreferences から取得します。

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);

// --- ePub出力設定 ---
boolean webConvertUpdatedOnly = prefs.getBoolean("web_convert_updated_only", false);

// --- 変換対象 ---
boolean webLatestOnly = prefs.getBoolean("web_latest_only", false);
int webLatestCount = Integer.parseInt(prefs.getString("web_latest_count", "1"));
boolean webModifiedOnly = prefs.getBoolean("web_modified_only", false);
boolean webModifiedTailOnly = prefs.getBoolean("web_modified_tail_only", false);
  • EditTextPreference(web_latest_count)はStringとして保存されるので、数値として使いたい場合は Integer.parseInt() が必要。
  • チェックボックスはすべて boolean として取得。

EditTextPreference(web_latest_count)はStringとして保存されるので、数値として使いたい場合は Integer.parseInt() が必要。

チェックボックスはすべて boolean として取得。

必要であれば、すべての設定をまとめた Settings クラスを用意して保持する形にもできます。必要でしたらお作りしますよ!

あなた:

次のコードをandroid向けにJavaで書き換えて。

////////////////////////////////////////////////////////////////
/** 複数ファイルを変換
 * @param dstPath srcFileがキャッシュで入力ファイルを同じ場所に出力先指定をする場合 */
private void convertFiles(Vector<File> vecSrcFiles, File dstPath) throws Exception {
    File[] srcFiles = new File[vecSrcFiles.size()];
    for (int i=0; i<srcFiles.length; i++) {
        srcFiles[i] = vecSrcFiles.get(i);
    }
    this.convertFiles(srcFiles, dstPath);
}

ChatGPT:

以下は、あなたの元のJavaコードを Android向けに書き換えたバージョン です。基本的に大きな変更は不要ですが、Androidでは Vector より ArrayList が推奨されているため、ArrayList<File> に書き換える例も併せて紹介します。

/**
 * 複数ファイルを変換
 * @param dstPath srcFileがキャッシュで入力ファイルと同じ場所に出力先指定をする場合
 */
private void convertFiles(Vector<File> vecSrcFiles, File dstPath) throws Exception {
    File[] srcFiles = new File[vecSrcFiles.size()];
    for (int i = 0; i < srcFiles.length; i++) {
        srcFiles[i] = vecSrcFiles.get(i);
    }
    this.convertFiles(srcFiles, dstPath);
}

このままでも Androidで問題なく動作します(java.io.File や Vector は標準Javaライブラリの一部として使えます)。

Androidでは Vector より ArrayList の使用が一般的です:

/**
 * 複数ファイルを変換
 * @param dstPath 出力先フォルダ(srcFileと同じ場所にも可)
 */
private void convertFiles(ArrayList<File> srcFileList, File dstPath) throws Exception {
    File[] srcFiles = srcFileList.toArray(new File[0]);
    this.convertFiles(srcFiles, dstPath);
}
ArrayList<File> srcFiles = new ArrayList<>();
srcFiles.add(new File("/path/to/file1.txt"));
srcFiles.add(new File("/path/to/file2.txt"));

File dstPath = new File("/path/to/output/");
convertFiles(srcFiles, dstPath);

ご希望があれば convertFiles(File[] srcFiles, File dstPath) の中身もAndroidに最適化して書きますので、気軽にどうぞ!

あなた:

androidでSwingWorkerのようなものはあるのか

ChatGPT:

はい、Androidには SwingWorker のような「バックグラウンド処理とUI更新を分離する仕組み」として、AsyncTask(非推奨)やHandler、ExecutorService、LiveData、ViewModel、Coroutine(Kotlin)など、さまざまな選択肢があります。

Javaらしく、バックグラウンドスレッドとUIスレッドの橋渡しに使える方法です。

ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());

executor.execute(() -> {
    // バックグラウンド処理(重い処理など)
    String result = "結果";

    // UIスレッドで結果を反映
    handler.post(() -> {
        // ここでUIを更新する
        textView.setText(result);
    });
});

これが SwingWorker#doInBackground → done() の流れに一番近い形です。

一昔前は AsyncTask が SwingWorker 的な存在でしたが、今は非推奨です:

private class MyTask extends AsyncTask<Void, Void, String> {
    @Override
    protected String doInBackground(Void... voids) {
        // バックグラウンド処理
        return "結果";
    }

    @Override
    protected void onPostExecute(String result) {
        // UI更新
        textView.setText(result);
    }
}
方法 特徴 使用推奨度
ExecutorService + Handler Java標準で簡単、安全、柔軟 ✅ 推奨
AsyncTask 簡単だが古い、非推奨 ❌ 避ける
LiveData, ViewModel MVVM向け、状態管理に便利 ✅ モダンUI向け(Jetpack)
Coroutine Kotlin専用、強力で簡潔 ✅ Kotlinの場合おすすめ

SwingWorkerの代わりをJavaで書きたいなら、ExecutorService + Handler が一番実用的かつ公式も推奨しています。 必要があればユーティリティクラスとして MyWorker 的なクラスを用意することもできますよ。やってみますか?

あなた:

次のコードをHandlerを使って書き換えて

/** 非同期タスクでダウンロードと変換処理 */
private class ConvertWebTask extends AsyncTask<Void, Void, Void> {
    private Context context;
    private List<String> urlList;
    private List<File> shortcutFiles;
    private File dstPath;

    public ConvertWebTask(Context context, List<String> urlList, List<File> shortcutFiles, File dstPath) {
        this.context = context;
        this.urlList = urlList;
        this.shortcutFiles = shortcutFiles;
        this.dstPath = dstPath;
    }

    @Override
    protected Void doInBackground(Void... voids) {
        try {
            for (int i = 0; i < urlList.size(); i++) {
                String urlString = urlList.get(i);
                File srcShortcutFile = (shortcutFiles != null && shortcutFiles.size() > i) ? shortcutFiles.get(i) : null;

                String ext = urlString.substring(urlString.lastIndexOf('.') + 1).toLowerCase();
                if (ext.equals("zip") || ext.equals("txtz") || ext.equals("rar")) {
                    // URLのファイル名を安全な形式に
                    String fileName = new File(new URI(urlString).getPath()).getName().replaceAll("[?*&|<>\"\\\\]", "_");
                    File srcFile = new File(dstPath, fileName);

                    LogAppender.println("出力先にダウンロードします: " + srcFile.getCanonicalPath());

                    if (!srcFile.getParentFile().exists()) {
                        srcFile.getParentFile().mkdirs();
                    }

                    // ダウンロード処理
                    try (BufferedInputStream bis = new BufferedInputStream(new URL(urlString).openStream(), 8192);
                         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(srcFile))) {
                        byte[] buffer = new byte[8192];
                        int bytesRead;
                        while ((bytesRead = bis.read(buffer)) != -1) {
                            bos.write(buffer, 0, bytesRead);
                        }
                    }

                    // 変換実行(仮のメソッド)
                    convertFiles(new File[]{srcFile}, dstPath);
                    continue;
                }

                // WebAozoraConverterの処理(仮)
                LogAppender.println("--------");
                LogAppender.append(urlString);
                LogAppender.println(" を読み込みます");

                WebAozoraConverter webConverter = WebAozoraConverter.createWebAozoraConverter(urlString, context);
                if (webConverter == null) {
                    LogAppender.append(urlString);
                    LogAppender.println(" は変換できませんでした");
                    continue;
                }
                int interval = 500;
                String Ua="Chrome";
                int beforeChapter = 0;
                float modifiedExpire = 0;
                boolean WebConvertUpdated= false;
                boolean WebModifiedOnly =false;
                boolean WebModifiedTail =false;
                boolean WebLageImage =false;
                srcFile = webConverter.convertToAozoraText(urlString, getCachePath(context), interval, modifiedExpire,
                WebConvertUpdated, WebModifiedOnly, WebModifiedTail,
                beforeChapter,Ua,WebLageImage);
               // File srcFile = webConverter.convertToAozoraText(urlString, getCachePath(context));
                if (srcFile == null) {
                    LogAppender.append(urlString);
                    LogAppender.println(" の変換をスキップまたは失敗しました");
                    continue;
                }

                // 変換処理実行
                convertFiles(new File[]{srcFile}, dstPath);
            }
        } catch (Exception e) {
            LogAppender.println("エラーが発生しました: " + e.getMessage());
        }
        return null;
    }
}

ChatGPT:

AsyncTask の代わりに Handler を使って非同期処理を行う場合は、基本的に別スレッドで処理を行い、UIスレッドに戻す際に Handler を使います。以下に ConvertWebTask を Handler + Thread ベースで書き換えた例を示します:

private void startConvertWebTask(Context context, List<String> urlList, List<File> shortcutFiles, File dstPath) {
    Handler handler = new Handler(Looper.getMainLooper());

    new Thread(() -> {
        try {
            for (int i = 0; i < urlList.size(); i++) {
                String urlString = urlList.get(i);
                File srcShortcutFile = (shortcutFiles != null && shortcutFiles.size() > i) ? shortcutFiles.get(i) : null;

                String ext = urlString.substring(urlString.lastIndexOf('.') + 1).toLowerCase();
                if (ext.equals("zip") || ext.equals("txtz") || ext.equals("rar")) {
                    String fileName = new File(new URI(urlString).getPath()).getName().replaceAll("[?*&|<>\"\\\\]", "_");
                    File srcFile = new File(dstPath, fileName);

                    String logMessage = "出力先にダウンロードします: " + srcFile.getCanonicalPath();
                    handler.post(() -> LogAppender.println(logMessage));

                    if (!srcFile.getParentFile().exists()) {
                        srcFile.getParentFile().mkdirs();
                    }

                    try (BufferedInputStream bis = new BufferedInputStream(new URL(urlString).openStream(), 8192);
                         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(srcFile))) {
                        byte[] buffer = new byte[8192];
                        int bytesRead;
                        while ((bytesRead = bis.read(buffer)) != -1) {
                            bos.write(buffer, 0, bytesRead);
                        }
                    }

                    convertFiles(new File[]{srcFile}, dstPath);
                    continue;
                }

                handler.post(() -> {
                    LogAppender.println("--------");
                    LogAppender.append(urlString);
                    LogAppender.println(" を読み込みます");
                });

                WebAozoraConverter webConverter = WebAozoraConverter.createWebAozoraConverter(urlString, context);
                if (webConverter == null) {
                    handler.post(() -> {
                        LogAppender.append(urlString);
                        LogAppender.println(" は変換できませんでした");
                    });
                    continue;
                }

                int interval = 500;
                String Ua = "Chrome";
                int beforeChapter = 0;
                float modifiedExpire = 0;
                boolean WebConvertUpdated = false;
                boolean WebModifiedOnly = false;
                boolean WebModifiedTail = false;
                boolean WebLageImage = false;

                File srcFile = webConverter.convertToAozoraText(
                    urlString,
                    getCachePath(context),
                    interval,
                    modifiedExpire,
                    WebConvertUpdated,
                    WebModifiedOnly,
                    WebModifiedTail,
                    beforeChapter,
                    Ua,
                    WebLageImage
                );

                if (srcFile == null) {
                    handler.post(() -> {
                        LogAppender.append(urlString);
                        LogAppender.println(" の変換をスキップまたは失敗しました");
                    });
                    continue;
                }

                convertFiles(new File[]{srcFile}, dstPath);
            }
        } catch (Exception e) {
            handler.post(() -> LogAppender.println("エラーが発生しました: " + e.getMessage()));
        }
    }).start();
}
  • Handler はメインスレッド(Looper.getMainLooper())にバインドしてログ出力などを UI スレッドで安全に実行。
  • メイン処理は new Thread(() -> {...}) で非同期実行。
  • LogAppender の呼び出しを handler.post() で包むことで UI スレッドに戻す。

Handler はメインスレッド(Looper.getMainLooper())にバインドしてログ出力などを UI スレッドで安全に実行。

メイン処理は new Thread(() -> {...}) で非同期実行。

LogAppender の呼び出しを handler.post() で包むことで UI スレッドに戻す。

必要であれば ProgressDialog などと連携してUIも更新できます。AsyncTask 非推奨以降、Handler か ExecutorService や Coroutine(Kotlin) の使用が推奨されてます。

この方式で問題ない?それとも別の非同期API(Executors など)で書きたい?

あなた:

ExecutorServiceを使った例も見たい

ChatGPT:

いいですね、ExecutorService を使うと AsyncTask より柔軟で、Handler よりもスッキリ書ける場面も多いです。以下は、さきほどの ConvertWebTask を ExecutorService で書き直した例です。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

private final ExecutorService executorService = Executors.newSingleThreadExecutor();

public void startConvertWebTask(Context context, List<String> urlList, List<File> shortcutFiles, File dstPath) {
    Handler handler = new Handler(Looper.getMainLooper());

    executorService.execute(() -> {
        try {
            for (int i = 0; i < urlList.size(); i++) {
                String urlString = urlList.get(i);
                File srcShortcutFile = (shortcutFiles != null && shortcutFiles.size() > i) ? shortcutFiles.get(i) : null;

                String ext = urlString.substring(urlString.lastIndexOf('.') + 1).toLowerCase();
                if (ext.equals("zip") || ext.equals("txtz") || ext.equals("rar")) {
                    String fileName = new File(new URI(urlString).getPath()).getName().replaceAll("[?*&|<>\"\\\\]", "_");
                    File srcFile = new File(dstPath, fileName);

                    handler.post(() -> {
                        try {
                            LogAppender.println("出力先にダウンロードします: " + srcFile.getCanonicalPath());
                        } catch (IOException e) {
                            LogAppender.println("ファイルパス取得エラー: " + e.getMessage());
                        }
                    });

                    if (!srcFile.getParentFile().exists()) {
                        srcFile.getParentFile().mkdirs();
                    }

                    try (BufferedInputStream bis = new BufferedInputStream(new URL(urlString).openStream(), 8192);
                         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(srcFile))) {
                        byte[] buffer = new byte[8192];
                        int bytesRead;
                        while ((bytesRead = bis.read(buffer)) != -1) {
                            bos.write(buffer, 0, bytesRead);
                        }
                    }

                    convertFiles(new File[]{srcFile}, dstPath);
                    continue;
                }

                handler.post(() -> {
                    LogAppender.println("--------");
                    LogAppender.append(urlString);
                    LogAppender.println(" を読み込みます");
                });

                WebAozoraConverter webConverter = WebAozoraConverter.createWebAozoraConverter(urlString, context);
                if (webConverter == null) {
                    handler.post(() -> {
                        LogAppender.append(urlString);
                        LogAppender.println(" は変換できませんでした");
                    });
                    continue;
                }

                int interval = 500;
                String Ua = "Chrome";
                int beforeChapter = 0;
                float modifiedExpire = 0;
                boolean WebConvertUpdated = false;
                boolean WebModifiedOnly = false;
                boolean WebModifiedTail = false;
                boolean WebLageImage = false;

                File srcFile = webConverter.convertToAozoraText(
                    urlString,
                    getCachePath(context),
                    interval,
                    modifiedExpire,
                    WebConvertUpdated,
                    WebModifiedOnly,
                    WebModifiedTail,
                    beforeChapter,
                    Ua,
                    WebLageImage
                );

                if (srcFile == null) {
                    handler.post(() -> {
                        LogAppender.append(urlString);
                        LogAppender.println(" の変換をスキップまたは失敗しました");
                    });
                    continue;
                }

                convertFiles(new File[]{srcFile}, dstPath);
            }
        } catch (Exception e) {
            handler.post(() -> LogAppender.println("エラーが発生しました: " + e.getMessage()));
        }
    });
}
  • ExecutorService はバックグラウンドスレッドを管理します。上記は1本のスレッドで逐次実行する構成(Executors.newSingleThreadExecutor())。
  • UIの更新は Handler を使ってメインスレッドに戻します(LogAppender など)。
  • 使い終わったら executorService.shutdown() するのが一般的ですが、何度も使うならアプリ終了までキープしてOK。

ExecutorService はバックグラウンドスレッドを管理します。上記は1本のスレッドで逐次実行する構成(Executors.newSingleThreadExecutor())。

UIの更新は Handler を使ってメインスレッドに戻します(LogAppender など)。

使い終わったら executorService.shutdown() するのが一般的ですが、何度も使うならアプリ終了までキープしてOK。

@Override
protected void onDestroy() {
    super.onDestroy();
    executorService.shutdownNow();
}

こちらの方がモダンAndroid向けにはおすすめです。もし Kotlin を使ってるなら Coroutine の方がさらにスマートだけど、Javaなら ExecutorService がベストに近い選択肢です。

他にも同様に非同期化したい処理があれば気軽に聞いてください!

あなた:

次のコードをandroid向けに書き換えて、mainactivity内にあるとする。

/** Web変換
 * @param vecUrlString 青空文庫テキストのzipまたは対応サイトのリンクURL
 * @param vecUrlSrcFile ショートカットファイルのURLならファイルが指定されている */
private void convertWeb(Vector<String> vecUrlString, Vector<File> vecUrlSrcFile, File dstPath) throws Exception {
    for (int i=0; i<vecUrlString.size(); i++) {
        String urlString = vecUrlString.get(i);
        File urSrcFile = null;
        if (vecUrlSrcFile != null && vecUrlSrcFile.size() > i) urSrcFile = vecUrlSrcFile.get(i);
        //URL変換 の最後が .zip .txtz .rar
        String ext = urlString.substring(urlString.lastIndexOf('.')+1).toLowerCase();
        if (ext.equals("zip") || ext.equals("txtz") || ext.equals("rar")) {

            String urlPath = urlString.substring(urlString.indexOf("//")+2).replaceAll("\\?\\*\\&\\|\\<\\>\"\\\\", "_");
            //青空zipのURLをキャッシュして変換
            //出力先 出力パスに保存
            File srcFile = new File(dstPath+"/"+new File(urlPath).getName());
            LogAppender.println("出力先にダウンロードします : "+srcFile.getCanonicalPath());
            srcFile.getParentFile().mkdirs();
            //ダウンロード
            BufferedInputStream bis = new BufferedInputStream(new URI(urlString).toURL().openStream(), 8192);
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(srcFile));
            //IOUtils.copy(bis, bos);
            bis.transferTo(bos);
            bos.close();
            bis.close();
            //変換実行
            this.convertFiles(new File[]{srcFile}, dstPath);

            continue;
        }

        try {
            LogAppender.println("--------");
            LogAppender.append(urlString);
            LogAppender.println(" を読み込みます");

            webConverter = WebAozoraConverter.createWebAozoraConverter(urlString, webConfigPath);
            if (webConverter == null) {
                LogAppender.append(urlString);
                LogAppender.println(" は変換できませんでした");
                continue;
            }

            int interval = 500;             
            try { interval = (int)(Float.parseFloat(jTextWebInterval.getText())*1000); } catch (Exception e) {
                throw new RuntimeException(e);
            }
            String Ua="";
            try { Ua = (jComboUa.getSelectedItem().toString()); } catch (Exception e) {
                throw new RuntimeException(e);
            }
            int beforeChapter = 0;
            if (this.jCheckWebBeforeChapter.isSelected()) {
                try { beforeChapter = Integer.parseInt(jTextWebBeforeChapterCount.getText()); } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            float modifiedExpire = 0;
            try { modifiedExpire = Float.parseFloat(jTextWebModifiedExpire.getText()); } catch (Exception e) {
                throw new RuntimeException(e);
            }
            //キャッシュパス
            if (!this.cachePath.isDirectory()) {
                this.cachePath.mkdirs();
                LogAppender.println("キャッシュパスを作成します : "+this.cachePath.getCanonicalPath());
            }
            if (!this.cachePath.isDirectory()) {
                LogAppender.println("キャッシュパスが作成できませんでした");
                return;
            }

            File srcFile = webConverter.convertToAozoraText(urlString, this.cachePath, interval, modifiedExpire,
                this.jCheckWebConvertUpdated.isSelected(), this.jCheckWebModifiedOnly.isSelected(), jCheckWebModifiedTail.isSelected(),
                beforeChapter,Ua,this.jCheckWebLageImage.isSelected());

            if (srcFile == null) {
                LogAppender.append(urlString);
                if (jCheckWebConvertUpdated.isSelected() && !webConverter.isUpdated()
                    || jCheckWebModifiedOnly.isSelected() && !webConverter.isUpdated())
                    LogAppender.println(" の変換をスキップしました");
                else if (webConverter.isCanceled())
                    LogAppender.println(" の変換をキャンセルしました");
                else
                    LogAppender.println(" は変換できませんでした");
                continue;
            }

            //エンコードを変換時のみUTF-8にする
            String encType = (String)jComboEncType.getSelectedItem();
            jComboEncType.setSelectedItem("UTF-8");
            int titleTypeIdx = jComboTitle.getSelectedIndex();
            jComboTitle.setSelectedIndex(0);
            boolean checkUseFileName = jCheckUseFileName.isSelected();
            jCheckUseFileName.setSelected(false);
            //コメント出力
            boolean commentPrint = jCheckCommentPrint.isSelected();
            jCheckCommentPrint.setSelected(true);
            boolean commentConvert = jCheckCommentConvert.isSelected();
            jCheckCommentConvert.setSelected(true);

            //表紙画像はconverted.pngで保存される 指定がない場合はそれを利用する
            Object coverItem = jComboCover.getSelectedItem();
            //入力ファイルと同じ表紙の指定の場合 ショートカットファイルのパスにファイルがあればファイルパスを指定に変更
            if (jComboCover.getSelectedIndex() == 1 && urSrcFile != null) {
                String coverFileName = AozoraEpub3.getSameCoverFileName(urSrcFile);
                jComboCover.setSelectedItem(coverFileName);
            }
            //同名のファイルが無い場合はconverted.pngを利用する設定に変更
            if (jComboCover.getSelectedIndex() == 0 || jComboCover.getSelectedIndex() == 1) jComboCover.setSelectedIndex(1);

            //変換処理実行
            convertFiles(new File[]{srcFile}, dstPath);

            //設定を戻す
            jComboEncType.setSelectedItem(encType);
            jComboTitle.setSelectedIndex(titleTypeIdx);
            jCheckUseFileName.setSelected(checkUseFileName);
            jCheckCommentPrint.setSelected(commentPrint);
            jCheckCommentConvert.setSelected(commentConvert);
            jComboCover.setSelectedItem(coverItem);

        } catch (Exception e) {
            LogAppender.println("エラーが発生しました : "+e.getMessage());
            throw new RuntimeException(e); 
        }
    }
}

/** キャッシュパスを取得 */
private File getCachePath()
{
    String cachePathString = this.jTextCachePath.getText().trim();
    if(cachePathString.isEmpty()) cachePathString = this.jarPath+".cache";
    return new File(cachePathString);
}
/** キャッシュパスを以下のファイルならtrue */
private boolean isCacheFile(File file)
{
    try {
        return file.getCanonicalPath().startsWith(this.getCachePath().getCanonicalPath());
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    //return false;
}

ChatGPT:

以下は、元の convertWeb メソッドを Android の MainActivity クラス内に移植した例です。UI 操作部分(Swing コンポーネント)を Android の SharedPreferences や Spinner, CheckBox, EditText に置き換えています。

public class MainActivity extends AppCompatActivity {

    private File cachePath;
    private File jarPath; // 必要であれば適切な初期化が必要

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 必要な初期化処理
    }

    private void convertWeb(List<String> urlList, List<File> shortcutFiles, File dstPath) throws Exception {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

        for (int i = 0; i < urlList.size(); i++) {
            String urlString = urlList.get(i);
            File srcShortcutFile = (shortcutFiles != null && shortcutFiles.size() > i) ? shortcutFiles.get(i) : null;

            String ext = urlString.substring(urlString.lastIndexOf('.') + 1).toLowerCase();
            if (ext.equals("zip") || ext.equals("txtz") || ext.equals("rar")) {
                String fileName = new File(new URI(urlString).getPath()).getName().replaceAll("[?*&|<>\"\\\\]", "_");
                File srcFile = new File(dstPath, fileName);

                LogAppender.println("出力先にダウンロードします : " + srcFile.getCanonicalPath());
                srcFile.getParentFile().mkdirs();

                try (BufferedInputStream bis = new BufferedInputStream(new URL(urlString).openStream(), 8192);
                     BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(srcFile))) {
                    byte[] buffer = new byte[8192];
                    int bytesRead;
                    while ((bytesRead = bis.read(buffer)) != -1) {
                        bos.write(buffer, 0, bytesRead);
                    }
                }

                convertFiles(new File[]{srcFile}, dstPath);
                continue;
            }

            LogAppender.println("--------");
            LogAppender.append(urlString);
            LogAppender.println(" を読み込みます");

            File webConfigPath = getCachePath(); // キャッシュパス取得
            WebAozoraConverter webConverter = WebAozoraConverter.createWebAozoraConverter(urlString, webConfigPath);
            if (webConverter == null) {
                LogAppender.append(urlString);
                LogAppender.println(" は変換できませんでした");
                continue;
            }

            int interval = (int)(prefs.getFloat("web_interval", 0.5f) * 1000);
            String ua = prefs.getString("web_ua", "Chrome");
            int beforeChapter = prefs.getBoolean("web_before_chapter", false)
                    ? prefs.getInt("web_before_chapter_count", 0) : 0;
            float modifiedExpire = prefs.getFloat("web_modified_expire", 0f);
            boolean convertUpdated = prefs.getBoolean("web_convert_updated_only", false);
            boolean modifiedOnly = prefs.getBoolean("web_modified_only", false);
            boolean modifiedTailOnly = prefs.getBoolean("web_modified_tail_only", false);
            boolean lageImage = prefs.getBoolean("web_large_image", false); // ← 適宜キー追加

            if (!getCachePath().isDirectory()) {
                getCachePath().mkdirs();
                LogAppender.println("キャッシュパスを作成します : " + getCachePath().getCanonicalPath());
            }

            File srcFile = webConverter.convertToAozoraText(
                    urlString, getCachePath(), interval, modifiedExpire,
                    convertUpdated, modifiedOnly, modifiedTailOnly,
                    beforeChapter, ua, lageImage
            );

            if (srcFile == null) {
                LogAppender.append(urlString);
                if ((convertUpdated && !webConverter.isUpdated()) ||
                    (modifiedOnly && !webConverter.isUpdated()))
                    LogAppender.println(" の変換をスキップしました");
                else if (webConverter.isCanceled())
                    LogAppender.println(" の変換をキャンセルしました");
                else
                    LogAppender.println(" は変換できませんでした");
                continue;
            }

            // 変換処理実行
            convertFiles(new File[]{srcFile}, dstPath);
        }
    }

    /** キャッシュパスの取得 */
    private File getCachePath() {
        if (cachePath == null) {
            String cachePathString = PreferenceManager.getDefaultSharedPreferences(this)
                    .getString("cache_path", jarPath + ".cache");
            cachePath = new File(cachePathString);
        }
        return cachePath;
    }

    /** キャッシュファイルであるかどうか判定 */
    private boolean isCacheFile(File file) {
        try {
            return file.getCanonicalPath().startsWith(getCachePath().getCanonicalPath());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /** 変換実行用:外部にある元の処理を仮定 */
    private void convertFiles(File[] srcFiles, File dstPath) {
        // 実装を別途用意(前述のHandler版/Executor版など)
    }
}
Swing / Java UI Android UI / 設定
jCheckWebConvertUpdated CheckBoxPreference("web_convert_updated_only")
jTextWebInterval.getText() SharedPreferences.getFloat("web_interval")
jComboUa.getSelectedItem() SharedPreferences.getString("web_ua")
jTextWebBeforeChapterCount SharedPreferences.getInt("web_before_chapter_count")
jTextWebModifiedExpire SharedPreferences.getFloat("web_modified_expire")
jComboEncType 省略 or 必要に応じて追加
jCheckUseFileName, jComboCover等 必要に応じて追加可能

必要なら、このメソッドを Handler や ExecutorService 経由で非同期実行する形にもできます。非同期実行の例が必要ですか?

あなた:

Handlerを使って

ChatGPT:

了解です!Handler を使って convertWeb を非同期に実行する方法を紹介します。 これは Android の UI スレッドをブロックせずに処理を実行 したいときに便利な方法です。

public class MainActivity extends AppCompatActivity {

    private HandlerThread backgroundThread;
    private Handler backgroundHandler;
    private Handler mainHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // バックグラウンド用のスレッドとハンドラ初期化
        backgroundThread = new HandlerThread("ConvertThread");
        backgroundThread.start();
        backgroundHandler = new Handler(backgroundThread.getLooper());
        mainHandler = new Handler(Looper.getMainLooper()); // UIスレッド用
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        backgroundThread.quitSafely();
    }

    public void startWebConvert(List<String> urlList, List<File> shortcutFiles, File dstPath) {
        backgroundHandler.post(() -> {
            try {
                convertWeb(urlList, shortcutFiles, dstPath);

                // UIに成功メッセージなどを表示したい場合
                mainHandler.post(() -> {
                    Toast.makeText(this, "変換完了!", Toast.LENGTH_SHORT).show();
                });

            } catch (Exception e) {
                mainHandler.post(() -> {
                    Toast.makeText(this, "変換中にエラーが発生しました: " + e.getMessage(), Toast.LENGTH_LONG).show();
                });
            }
        });
    }

    private void convertWeb(List<String> urlList, List<File> shortcutFiles, File dstPath) throws Exception {
        // ← ここに前述の convertWeb の中身をそのまま置いてOK
    }

    private File getCachePath() {
        // ← 前述の getCachePath() と同じ
    }

    private boolean isCacheFile(File file) {
        // ← 前述の isCacheFile() と同じ
    }

    private void convertFiles(File[] srcFiles, File dstPath) {
        // ← 必要に応じて実装
    }
}

たとえばボタンを押したときに変換を開始したいなら:

Button convertButton = findViewById(R.id.convertButton);
convertButton.setOnClickListener(v -> {
    List<String> urls = Arrays.asList("https://example.com/sample.zip");
    List<File> shortcutFiles = Arrays.asList((File) null); // または該当ファイル
    File dstPath = new File(getFilesDir(), "output");

    startWebConvert(urls, shortcutFiles, dstPath);
});
  • HandlerThread:バックグラウンドスレッドで処理を行う専用のスレッド。
  • Handler:バックグラウンド処理 (backgroundHandler) と UIスレッドへの通知 (mainHandler) を分けて管理。
  • Toast.makeText(...).show():UI更新は mainHandler.post() 内で実行。

HandlerThread:バックグラウンドスレッドで処理を行う専用のスレッド。

Handler:バックグラウンド処理 (backgroundHandler) と UIスレッドへの通知 (mainHandler) を分けて管理。

Toast.makeText(...).show():UI更新は mainHandler.post() 内で実行。

必要があれば、ProgressBar 連動やキャンセル対応なども追加できます!やってみますか?

あなた:

AsyncTaskが非推奨なので改善したい

/** Web変換 */
private void convertWeb(List<String> urlList, List<File> shortcutFiles, File dstPath) {
    new ConvertWebTask(this, urlList, shortcutFiles, dstPath).execute();
}

/** 非同期タスクでダウンロードと変換処理 */
private class ConvertWebTask extends AsyncTask<Void, Void, Void> {
    private Context context;
    private List<String> urlList;
    private List<File> shortcutFiles;
    private File dstPath;

    public ConvertWebTask(Context context, List<String> urlList, List<File> shortcutFiles, File dstPath) {
        this.context = context;
        this.urlList = urlList;
        this.shortcutFiles = shortcutFiles;
        this.dstPath = dstPath;
    }

    @Override
    protected Void doInBackground(Void... voids) {
        try {
            for (int i = 0; i < urlList.size(); i++) {
                String urlString = urlList.get(i);
                File srcShortcutFile = (shortcutFiles != null && shortcutFiles.size() > i) ? shortcutFiles.get(i) : null;

                String ext = urlString.substring(urlString.lastIndexOf('.') + 1).toLowerCase();
                if (ext.equals("zip") || ext.equals("txtz") || ext.equals("rar")) {
                    // URLのファイル名を安全な形式に
                    String fileName = new File(new URI(urlString).getPath()).getName().replaceAll("[?*&|<>\"\\\\]", "_");
                    File srcFile = new File(dstPath, fileName);

                    LogAppender.println("出力先にダウンロードします: " + srcFile.getCanonicalPath());

                    if (!srcFile.getParentFile().exists()) {
                        srcFile.getParentFile().mkdirs();
                    }

                    // ダウンロード処理
                    try (BufferedInputStream bis = new BufferedInputStream(new URL(urlString).openStream(), 8192);
                         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(srcFile))) {
                        byte[] buffer = new byte[8192];
                        int bytesRead;
                        while ((bytesRead = bis.read(buffer)) != -1) {
                            bos.write(buffer, 0, bytesRead);
                        }
                    }

                    // 変換実行(仮のメソッド)
                    convertFiles(new File[]{srcFile}, dstPath);
                    continue;
                }

                // WebAozoraConverterの処理(仮)
                LogAppender.println("--------");
                LogAppender.append(urlString);
                LogAppender.println(" を読み込みます");

                WebAozoraConverter webConverter = WebAozoraConverter.createWebAozoraConverter(urlString, context);
                if (webConverter == null) {
                    LogAppender.append(urlString);
                    LogAppender.println(" は変換できませんでした");
                    continue;
                }
                int interval = 500;
                String Ua="Chrome";
                int beforeChapter = 0;
                float modifiedExpire = 0;
                boolean WebConvertUpdated= false;
                boolean WebModifiedOnly =false;
                boolean WebModifiedTail =false;
                boolean WebLageImage =false;
                srcFile = webConverter.convertToAozoraText(urlString, getCachePath(context), interval, modifiedExpire,
                WebConvertUpdated, WebModifiedOnly, WebModifiedTail,
                beforeChapter,Ua,WebLageImage);
               // File srcFile = webConverter.convertToAozoraText(urlString, getCachePath(context));
                if (srcFile == null) {
                    LogAppender.append(urlString);
                    LogAppender.println(" の変換をスキップまたは失敗しました");
                    continue;
                }

                // 変換処理実行
                convertFiles(new File[]{srcFile}, dstPath);
            }
        } catch (Exception e) {
            LogAppender.println("エラーが発生しました: " + e.getMessage());
        }
        return null;
    }
}

ChatGPT:

AsyncTask が非推奨になった現在、Handler + HandlerThread、Executors、または Coroutine(Kotlin) などが推奨されます。

ここでは、HandlerThread + Handler を使って AsyncTask 相当のコードをモダンに置き換えた例を提示します。

public class MainActivity extends AppCompatActivity {

    private HandlerThread convertThread;
    private Handler convertHandler;
    private Handler mainHandler;

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

        // バックグラウンド用スレッド初期化
        convertThread = new HandlerThread("WebConvertThread");
        convertThread.start();
        convertHandler = new Handler(convertThread.getLooper());
        mainHandler = new Handler(Looper.getMainLooper());
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        convertThread.quitSafely();
    }

    /** Web変換開始(UIから呼び出す) */
    private void convertWeb(List<String> urlList, List<File> shortcutFiles, File dstPath) {
        convertHandler.post(() -> {
            try {
                for (int i = 0; i < urlList.size(); i++) {
                    String urlString = urlList.get(i);
                    File srcShortcutFile = (shortcutFiles != null && shortcutFiles.size() > i)
                            ? shortcutFiles.get(i) : null;

                    String ext = urlString.substring(urlString.lastIndexOf('.') + 1).toLowerCase();
                    File srcFile;

                    if (ext.equals("zip") || ext.equals("txtz") || ext.equals("rar")) {
                        String fileName = new File(new URI(urlString).getPath())
                                .getName().replaceAll("[?*&|<>\"\\\\]", "_");
                        srcFile = new File(dstPath, fileName);

                        LogAppender.println("出力先にダウンロードします: " + srcFile.getCanonicalPath());

                        if (!srcFile.getParentFile().exists()) {
                            srcFile.getParentFile().mkdirs();
                        }

                        try (BufferedInputStream bis = new BufferedInputStream(new URL(urlString).openStream(), 8192);
                             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(srcFile))) {
                            byte[] buffer = new byte[8192];
                            int bytesRead;
                            while ((bytesRead = bis.read(buffer)) != -1) {
                                bos.write(buffer, 0, bytesRead);
                            }
                        }

                        convertFiles(new File[]{srcFile}, dstPath);
                        continue;
                    }

                    LogAppender.println("--------");
                    LogAppender.append(urlString);
                    LogAppender.println(" を読み込みます");

                    WebAozoraConverter webConverter = WebAozoraConverter.createWebAozoraConverter(urlString, this);
                    if (webConverter == null) {
                        LogAppender.append(urlString);
                        LogAppender.println(" は変換できませんでした");
                        continue;
                    }

                    // 任意の設定を適用
                    int interval = 500;
                    String Ua = "Chrome";
                    int beforeChapter = 0;
                    float modifiedExpire = 0;
                    boolean webConvertUpdated = false;
                    boolean webModifiedOnly = false;
                    boolean webModifiedTail = false;
                    boolean webLageImage = false;

                    srcFile = webConverter.convertToAozoraText(
                            urlString,
                            getCachePath(this),
                            interval,
                            modifiedExpire,
                            webConvertUpdated,
                            webModifiedOnly,
                            webModifiedTail,
                            beforeChapter,
                            Ua,
                            webLageImage
                    );

                    if (srcFile == null) {
                        LogAppender.append(urlString);
                        LogAppender.println(" の変換をスキップまたは失敗しました");
                        continue;
                    }

                    convertFiles(new File[]{srcFile}, dstPath);
                }

                mainHandler.post(() ->
                        Toast.makeText(this, "変換が完了しました", Toast.LENGTH_SHORT).show()
                );

            } catch (Exception e) {
                LogAppender.println("エラーが発生しました: " + e.getMessage());
                mainHandler.post(() ->
                        Toast.makeText(this, "エラー: " + e.getMessage(), Toast.LENGTH_LONG).show()
                );
            }
        });
    }

    /** キャッシュパス取得 */
    private File getCachePath(Context context) {
        File cacheDir = new File(context.getFilesDir(), "web_cache");
        if (!cacheDir.exists()) {
            cacheDir.mkdirs();
        }
        return cacheDir;
    }

    /** 変換処理本体(仮) */
    private void convertFiles(File[] srcFiles, File dstPath) {
        // 実際のEPUB変換処理を実装
    }
}
convertWeb(urlList, shortcutFiles, new File(getFilesDir(), "output"));
  • AsyncTask より構造が明快(非推奨を回避)
  • UI スレッドの更新は mainHandler.post() で安全に行える
  • 長時間処理でも中断せずに安全に実行される

AsyncTask より構造が明快(非推奨を回避)

UI スレッドの更新は mainHandler.post() で安全に行える

長時間処理でも中断せずに安全に実行される

必要に応じて、プログレス表示やキャンセル処理を追加することもできます。やってみますか?

あなた:

                                                   Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.os.Handler.post(java.lang.Runnable)' on a null object reference
                                                                                                    at io.github.kyukyunyorituryo.aozoraepub3.MainActivity.convertWeb

ChatGPT:

ありがとうございます。エラーの内容を見ると:

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.os.Handler.post(java.lang.Runnable)' on a null object reference

つまり、mainHandler.post() を呼び出したときに mainHandler が null になっているということです。

mainHandler を初期化せずに convertWeb() を呼び出した可能性があります。

mainHandler と convertHandler の 初期化処理が確実に呼ばれているか を確認してください。特に onCreate() の中で以下の初期化が必要です:

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

    // ★必須:ここで初期化されていること
    convertThread = new HandlerThread("WebConvertThread");
    convertThread.start();
    convertHandler = new Handler(convertThread.getLooper());

    mainHandler = new Handler(Looper.getMainLooper());
}

もし convertWeb() が onCreate() より先に呼ばれてしまう可能性があるなら、mainHandler が null でないことをチェックしましょう:

if (mainHandler == null) {
    mainHandler = new Handler(Looper.getMainLooper());
}
  • mainHandler = new Handler(Looper.getMainLooper()); を onCreate() で初期化しているか?
  • convertWeb() を onCreate() より前に呼び出していないか?
  • mainHandler.post() を使う前に null チェックしているか?

mainHandler = new Handler(Looper.getMainLooper()); を onCreate() で初期化しているか?

convertWeb() を onCreate() より前に呼び出していないか?

mainHandler.post() を使う前に null チェックしているか?

必要なら、convertWeb() の前に initHandlers() みたいな初期化関数を分けて作ってもいいですね。

private void initHandlers() {
    if (convertHandler == null) {
        convertThread = new HandlerThread("WebConvertThread");
        convertThread.start();
        convertHandler = new Handler(convertThread.getLooper());
    }
    if (mainHandler == null) {
        mainHandler = new Handler(Looper.getMainLooper());
    }
}

そして convertWeb() の最初で呼び出す:

private void convertWeb(...) {
    initHandlers();
    ...
}

気になる箇所が他にもあれば、コードを見せてくれれば一緒に見ます!

あなた:

listから選択後に編集可能にしたい

    <ListPreference
        android:key="UserAgent"
        android:title="ユーザーエージェント"
        android:summary="ユーザーエージェントを設定します"
        android:entries="@array/ua_entries"
        android:entryValues="@array/ua_values"
        android:defaultValue="" />

ChatGPT:

ListPreference だけでは、選択後にユーザーが 自由に編集(カスタム値入力) することはできません。 ですが、以下のような手順で 選択 + 編集 を両立するカスタムUIを作ることが可能です。

Androidの設定画面で「リストから選択 + 自由入力可」にする方法は、以下のような流れで実装できます:

<EditTextPreference
    android:key="UserAgent"
    android:title="ユーザーエージェント"
    android:summary="ユーザーエージェントを設定します(選択または編集)"
    android:defaultValue=""
    android:dialogTitle="ユーザーエージェントを入力または選択してください"/>
EditTextPreference uaPref = findPreference("UserAgent");
if (uaPref != null) {
    uaPref.setOnPreferenceClickListener(preference -> {
        // 選択肢(ListPreferenceのような動き)
        String[] uaEntries = getResources().getStringArray(R.array.ua_entries);
        String[] uaValues = getResources().getStringArray(R.array.ua_values);

        new AlertDialog.Builder(requireContext())
            .setTitle("ユーザーエージェントを選択")
            .setItems(uaEntries, (dialog, which) -> {
                String selectedUa = uaValues[which];
                uaPref.setText(selectedUa); // 値をセット
            })
            .setNegativeButton("キャンセル", null)
            .setNeutralButton("カスタム入力", (dialog, which) -> {
                // 標準のEditTextダイアログを開く
                uaPref.performClick();
            })
            .show();

        return true; // ハンドル済み
    });
}
  • ユーザーは リストから選ぶ か、
  • 「カスタム入力」を選んで 手動で入力 できるようになります。

ユーザーは リストから選ぶ か、

「カスタム入力」を選んで 手動で入力 できるようになります。

方法 内容
ListPreference 単体 選択肢だけ、カスタム不可 ❌
EditTextPreference + AlertDialog 選択+カスタム入力 ✅

もし PreferenceFragmentCompat の中身も一緒に書いてほしい場合、気軽に言ってください!コピペできるように組み立てます。

あなた:

PreferenceFragmentCompatのコード例

ChatGPT:

以下は、PreferenceFragmentCompat を使って 「ユーザーエージェントをリストから選択+カスタム編集可能」にするコード例 です。 XMLはそのままで、Javaで選択肢+編集ダイアログを実装しています。

<PreferenceScreen xmlns:android="">http://schemas.android.com/apk/res/android">

    <EditTextPreference
        android:key="UserAgent"
        android:title="ユーザーアージェント"
        android:summary="ユーザーエージェントを設定します(選択または編集)"
        android:defaultValue=""
        android:dialogTitle="ユーザーエージェントを入力" />

</PreferenceScreen>
<resources>
    <string-array name="ua_entries">
        <item>Chrome (Windows)</item>
        <item>Firefox (Linux)</item>
        <item>Safari (iOS)</item>
    </string-array>

    <string-array name="ua_values">
        <item>Mozilla/5.0 (Windows NT 10.0; Win64; x64)...</item>
        <item>Mozilla/5.0 (X11; Ubuntu; Linux x86_64)...</item>
        <item>Mozilla/5.0 (iPhone; CPU iPhone OS 13_5)...</item>
    </string-array>
</resources>
public class MainPreferenceFragment extends PreferenceFragmentCompat {

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        setPreferencesFromResource(R.xml.preferences, rootKey);

        EditTextPreference uaPref = findPreference("UserAgent");
        if (uaPref != null) {
            uaPref.setOnPreferenceClickListener(preference -> {
                String[] uaEntries = getResources().getStringArray(R.array.ua_entries);
                String[] uaValues = getResources().getStringArray(R.array.ua_values);

                new AlertDialog.Builder(requireContext())
                        .setTitle("ユーザーエージェントを選択")
                        .setItems(uaEntries, (dialog, which) -> {
                            // 選択肢から選んだ値を反映
                            uaPref.setText(uaValues[which]);
                        })
                        .setNegativeButton("キャンセル", null)
                        .setNeutralButton("カスタム入力", (dialog, which) -> {
                            // 本来のEditTextPreferenceの編集ダイアログを表示
                            uaPref.performClick();
                        })
                        .show();

                // trueにしてクリック処理を上書き
                return true;
            });
        }
    }
}
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportFragmentManager()
            .beginTransaction()
            .replace(android.R.id.content, new MainPreferenceFragment())
            .commit();
    }
}
  1. ユーザーが「ユーザーエージェント」設定をタップ
  2. 「選択肢リスト」が出る(Chrome, Firefox, Safari など)
  3. 「カスタム入力」を選ぶと自由入力が可能

ユーザーが「ユーザーエージェント」設定をタップ

「選択肢リスト」が出る(Chrome, Firefox, Safari など)

「カスタム入力」を選ぶと自由入力が可能

必要があれば Kotlin 版も出せます!お気軽にどうぞ。

あなた:

Unexpected token

ChatGPT:

「Unexpected token」エラーが出ている場合、どこで出たのか(Java?XML?他?)によって原因と対応が変わります。 前回のコードをそのまま使った場合、次のような原因が考えられます:

setNeutralButton("カスタム入力", (dialog, which) -> { ... }) のラムダ記法は Java 8 以上が必要です。 Java 7 以前のコードでは次のように書き直してください:

.setNeutralButton("カスタム入力", (dialog, which) -> {
    uaPref.performClick();
})
.setNeutralButton("カスタム入力", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
        uaPref.performClick();
    }
})

同様に .setItems(..., (dialog, which) -> {...}) も修正が必要です:

.setItems(uaEntries, new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
        uaPref.setText(uaValues[which]);
    }
})

一部のバージョンで EditTextPreference に対する performClick() が正常に動かないことがあります。 その場合、代わりに DialogPreference を手動で表示する処理が必要です。が、たいていは問題ないはずです。

arrays.xml や preferences.xml に > の閉じ忘れ、<item> のネストミスがあると「Unexpected token」になります。

エラーが出た具体的な場所(Java? XML? IDEの警告?)を教えてもらえると、もっとピンポイントで直せます。

もしよければ、今出ているエラーメッセージの全文(どこで何行目)をコピペしてくれますか? それに合わせて修正コードを提示します!

あなた:

Unknown class: 'uaPref'

ChatGPT:

ありがとうございます!Unknown class: 'uaPref' というエラーは、Javaコード内で uaPref という変数を使っているのに、その変数が宣言されていない(見つけられていない)という意味です。

先ほどの PreferenceFragmentCompat の例では、次のようなコードがありました:

EditTextPreference uaPref = findPreference("UserAgent");

この uaPref が正しく定義されていない、あるいは findPreference が null を返しているのが原因です。

uaPref を使う前に null チェックを追加しましょう:

EditTextPreference uaPref = findPreference("UserAgent");
if (uaPref != null) {
    // ここにクリック処理やダイアログ処理を書く
}

PreferenceFragmentCompat の中では findPreference() は onCreatePreferences() のあとでしか使えません。

正しい書き方の例:

public class SettingsFragment extends PreferenceFragmentCompat {

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        setPreferencesFromResource(R.xml.preferences, rootKey);

        EditTextPreference uaPref = findPreference("UserAgent");
        if (uaPref != null) {
            uaPref.setOnPreferenceClickListener(preference -> {
                showUserAgentDialog(uaPref);
                return true;
            });
        }
    }

    private void showUserAgentDialog(EditTextPreference uaPref) {
        String[] uaEntries = getResources().getStringArray(R.array.ua_entries);
        String[] uaValues = getResources().getStringArray(R.array.ua_values);

        new AlertDialog.Builder(requireContext())
            .setTitle("ユーザーエージェントを選択")
            .setItems(uaEntries, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    uaPref.setText(uaValues[which]);
                }
            })
            .setNeutralButton("カスタム入力", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    uaPref.performClick();
                }
            })
            .show();
    }
}
チェック項目 対応方法
findPreference() が null になる キーが正しいか、呼び出し位置が onCreatePreferences 以降か確認
uaPref が未定義 EditTextPreference uaPref = findPreference("UserAgent"); を追加
ラムダ使用の環境が古い ラムダを匿名クラスに書き直す

よければ preferences.xml の中身も貼っていただけたら、さらに確認できます!