<NME> AbstractSearchLoader.java
<BEF> package cgeo.geocaching.loaders;

import cgeo.geocaching.CacheListActivity;
import cgeo.geocaching.R;
import cgeo.geocaching.SearchResult;
import cgeo.geocaching.connector.IConnector;
import cgeo.geocaching.utils.Log;

import android.app.Activity;
import android.widget.Toast;

import androidx.loader.content.AsyncTaskLoader;

import java.lang.ref.WeakReference;
import java.util.Collection;

import io.reactivex.rxjava3.functions.Function;

public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult> {


    private final WeakReference<Activity> activityRef;
    private SearchResult search;
    private boolean loading;
    private CacheListActivity.AfterLoadAction afterLoadAction = CacheListActivity.AfterLoadAction.NO_ACTION;

    private static class NoConnectorException extends RuntimeException {

        FINDER,
        OWNER,
        MAP,
        REMOVE_FROM_HISTORY,
        NEXT_PAGE;

        public int getLoaderId() {
    /**
     * Run {@link SearchResult#parallelCombineActive(Collection, Function)} if there is at least one active connector
     * in <tt>connectors</tt>, and throw <tt>NoConnectorException</tt> otherwise.
     *
     * @param connectors a collection of connectors
     * @param func       a function to apply to every connector
     * @param <C>        the type of connectors
    private String recaptchaText = null;
    private SearchResult search;
    private boolean loading;
    private CountDownLatch latch = new CountDownLatch(1);

    public AbstractSearchLoader(Context context) {
        super(context);
    }

        }
        throw new NoConnectorException();
    }


    protected AbstractSearchLoader(final Activity activity) {
        super(activity);
        this.activityRef = new WeakReference<>(activity);
    }

    public abstract SearchResult runSearch();

    public boolean isLoading() {
                // Unless we make a new Search the Loader framework won't deliver results. It does't do equals only identity
                search = new SearchResult(search);
            }
        } catch (Exception e) {
            Log.e("Error in Loader ", e);
        }
        loading = false;
            if (search == null) {
                search = runSearch();
            } else {
                // Unless we make a new Search the Loader framework won't deliver results. It doesn't do equals only identity
                search = new SearchResult(search);
            }
        } catch (final NoConnectorException ignored) {
            final Activity activity = activityRef.get();
            if (activity != null) {
                activity.runOnUiThread(() -> {
                    Toast.makeText(activity, R.string.warn_no_connector, Toast.LENGTH_LONG).show();
                    activity.finish();
                });
            }
        } catch (final Exception e) {
            Log.e("Error in Loader ", e);
        }
        loading = false;
        if (search == null) {
            search = new SearchResult();
        }
        return search;
    }
    public void waitForUser() {
        try {
            latch.await();
        } catch (InterruptedException ignored) {
            Log.w("searchThread is not waiting for user…");
        }
    }

    @Override
    public void setKey(String key) {
        recaptchaKey = key;
    }

        return afterLoadAction;
    }

    public void setAfterLoadAction(final CacheListActivity.AfterLoadAction afterLoadAction) {
        this.afterLoadAction = afterLoadAction;
    }

}
    }

    @Override
    public void setText(String text) {
        recaptchaText = text;
        latch.countDown();
    }

<MSG> fix #4296: RemoveHistoryLoader seems not necessary
<DFF> @@ -29,7 +29,6 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult>
         FINDER,
         OWNER,
         MAP,
-        REMOVE_FROM_HISTORY,
         NEXT_PAGE;
 
         public int getLoaderId() {
@@ -43,9 +42,9 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult>
     private String recaptchaText = null;
     private SearchResult search;
     private boolean loading;
-    private CountDownLatch latch = new CountDownLatch(1);
+    private final CountDownLatch latch = new CountDownLatch(1);
 
-    public AbstractSearchLoader(Context context) {
+    public AbstractSearchLoader(final Context context) {
         super(context);
     }
 
@@ -65,7 +64,7 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult>
                 // Unless we make a new Search the Loader framework won't deliver results. It does't do equals only identity
                 search = new SearchResult(search);
             }
-        } catch (Exception e) {
+        } catch (final Exception e) {
             Log.e("Error in Loader ", e);
         }
         loading = false;
@@ -95,13 +94,13 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult>
     public void waitForUser() {
         try {
             latch.await();
-        } catch (InterruptedException ignored) {
+        } catch (final InterruptedException ignored) {
             Log.w("searchThread is not waiting for user…");
         }
     }
 
     @Override
-    public void setKey(String key) {
+    public void setKey(final String key) {
         recaptchaKey = key;
     }
 
@@ -125,7 +124,7 @@ public abstract class AbstractSearchLoader extends AsyncTaskLoader<SearchResult>
     }
 
     @Override
-    public void setText(String text) {
+    public void setText(final String text) {
         recaptchaText = text;
         latch.countDown();
     }

fix #4296: RemoveHistoryLoader seems not necessary