<NME> ConnectorFactory.java
<BEF> package cgeo.geocaching.connector;

import cgeo.geocaching.R;
import cgeo.geocaching.SearchResult;
import cgeo.geocaching.connector.al.ALConnector;
import cgeo.geocaching.connector.capability.ICredentials;
import cgeo.geocaching.connector.capability.ILogin;
import cgeo.geocaching.connector.capability.ISearchByFilter;
import cgeo.geocaching.connector.capability.ISearchByNextPage;
import cgeo.geocaching.connector.capability.ISearchByViewPort;
import cgeo.geocaching.connector.ec.ECConnector;
import cgeo.geocaching.connector.ga.GeocachingAustraliaConnector;
import cgeo.geocaching.connector.gc.GCConnector;
import cgeo.geocaching.connector.ge.GeopeitusConnector;
import cgeo.geocaching.connector.internal.InternalConnector;
import cgeo.geocaching.connector.oc.OCApiConnector.ApiSupport;
import cgeo.geocaching.connector.oc.OCApiLiveConnector;
import cgeo.geocaching.connector.oc.OCCZConnector;
import cgeo.geocaching.connector.oc.OCDEConnector;
import cgeo.geocaching.connector.su.SuConnector;
import cgeo.geocaching.connector.tc.TerraCachingConnector;
import cgeo.geocaching.connector.trackable.GeokretyConnector;
import cgeo.geocaching.connector.trackable.TrackableBrand;
import cgeo.geocaching.connector.trackable.TrackableConnector;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.annotation.NonNull;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;

public final class ConnectorFactory {
    private static final UnknownConnector UNKNOWN_CONNECTOR = new UnknownConnector();
    private static final IConnector[] CONNECTORS = new IConnector[] {
            GCConnector.getInstance(),
            ECConnector.getInstance(),
            new OCApiLiveConnector("opencaching.de", "www.opencaching.de", "OC", "CC BY-NC-ND, alle Logeinträge © jeweiliger Autor",
import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
import org.apache.commons.lang3.StringUtils;
            new GeopeitusConnector(),
            new WaymarkingConnector(),
            UNKNOWN_CONNECTOR // the unknown connector MUST be the last one
    };

    @NonNull public static final UnknownTrackableConnector UNKNOWN_TRACKABLE_CONNECTOR = new UnknownTrackableConnector();
    private static final TrackableConnector[] TRACKABLE_CONNECTORS = new TrackableConnector[] {
            new GeokretyConnector(), // GK must be first, as it overlaps with the secret codes of travel bugs
            TravelBugConnector.getInstance(),
            UNKNOWN_TRACKABLE_CONNECTOR // must be last
    };

    private static final ISearchByViewPort[] searchByViewPortConns = getMatchingConnectors(ISearchByViewPort.class);

    private static final ISearchByCenter[] searchByCenterConns = getMatchingConnectors(ISearchByCenter.class);

    private static final ISearchByKeyword[] searchByKeywordConns = getMatchingConnectors(ISearchByKeyword.class);

    @SuppressWarnings("unchecked")
    private static <T extends IConnector> T[] getMatchingConnectors(final Class<T> clazz) {
        final List<T> matching = new ArrayList<T>();
        for (final IConnector connector : CONNECTORS) {
            if (clazz.isInstance(connector)) {
                matching.add((T) connector);
            }
        }
        T[] result = (T[]) Array.newInstance(clazz, matching.size());
        return matching.toArray(result);
    }

    public static IConnector[] getConnectors() {
        return CONNECTORS;
    }

    public static ISearchByCenter[] getSearchByCenterConnectors() {
        return searchByCenterConns;
    }

    public static ISearchByKeyword[] getSearchByKeywordConnectors() {
        return searchByKeywordConns;
    }


    @NonNull
    private static final Collection<ISearchByViewPort> searchByViewPortConns = getMatchingConnectors(ISearchByViewPort.class);

    @NonNull
    private static final Collection<ISearchByNextPage> searchByNextPageConns = getMatchingConnectors(ISearchByNextPage.class);

    @NonNull
    private static final Map<GeocacheFilterType, Collection<ISearchByFilter>> SEARCH_BY_FILTER_CONNECTOR_MAP = new HashMap<>();

    static {
        SEARCH_BY_FILTER_CONNECTOR_MAP.put(null, getMatchingConnectors(ISearchByFilter.class));
        for (GeocacheFilterType filterCap : GeocacheFilterType.values()) {
            final Collection<ISearchByFilter> connectors = getMatchingConnectors(ISearchByFilter.class, c -> c.getFilterCapabilities().contains(filterCap));
            if (!connectors.isEmpty()) {
                SEARCH_BY_FILTER_CONNECTOR_MAP.put(filterCap, connectors);
            }
        }
    }

    private static boolean forceRelog = false; // c:geo needs to log into cache providers

    private ConnectorFactory() {
        // utility class
    }

    @NonNull
    @SuppressWarnings("unchecked")
    private static <T extends IConnector> Collection<T> getMatchingConnectors(final Class<T> clazz) {
        return getMatchingConnectors(clazz, null);
    }

    @NonNull
    @SuppressWarnings("unchecked")
    private static <T extends IConnector> Collection<T> getMatchingConnectors(final Class<T> clazz, final Func1<T, Boolean> filter) {
        final List<T> matching = new ArrayList<>();
        for (final IConnector connector : CONNECTORS) {
            if (clazz.isInstance(connector) && (filter == null || filter.call((T) connector))) {
                matching.add((T) connector);
            }
        }
        return Collections.unmodifiableCollection(matching);
    }

    @NonNull
    public static Collection<IConnector> getConnectors() {
        return CONNECTORS;
    }

    @NonNull
    public static Collection<ISearchByNextPage> getSearchByNextPageConnectors() {
        return searchByNextPageConns;
    }

    @NonNull
    public static Collection<ISearchByFilter> getSearchByFilterConnectors() {
        return getSearchByFilterConnectors(null);
    }

    @NonNull
    public static Collection<ISearchByFilter> getSearchByFilterConnectors(final GeocacheFilterType type) {
        final Collection<ISearchByFilter> result = SEARCH_BY_FILTER_CONNECTOR_MAP.get(type);
        return result == null ? Collections.emptyList() : result;
    }

    @NonNull
    public static ILogin[] getActiveLiveConnectors() {
        final List<ILogin> liveConns = new ArrayList<>();
        for (final IConnector conn : CONNECTORS) {
            if (conn instanceof ILogin && conn.isActive()) {
                liveConns.add((ILogin) conn);
            }
        }
        return liveConns.toArray(new ILogin[liveConns.size()]);
    }

    @NonNull
    public static IConnector[] getActiveConnectorsWithValidCredentials() {
        final List<IConnector> credConns = new ArrayList<>();
        for (final IConnector conn : CONNECTORS) {
        return null;
    }

    public static TrackableConnector[] getTrackableConnectors() {
        return TRACKABLE_CONNECTORS;
    }

    @NonNull
    public static List<IConnector> getActiveConnectors() {
        final List<IConnector> activeConnectors = new ArrayList<>();
        for (final IConnector conn : CONNECTORS) {
            if (conn.isActive()) {
                activeConnectors.add(conn);
            }
        }
        return activeConnectors;
    }

    public static boolean anyConnectorActive() {
        for (final IConnector conn : CONNECTORS) {
            if (conn.isActive()) {
                return true;
            }
        }
        return false;
    }

    public static boolean anyTrackableConnectorActive() {
        for (final TrackableConnector conn : TRACKABLE_CONNECTORS) {
            if (conn.isActive()) {
                return true;
            }
        }
        return false;
    }

    public static boolean canHandle(@Nullable final String geocode) {
        if (geocode == null) {
            return false;
        }
        if (isInvalidGeocode(geocode)) {
            return false;
        }
        for (final IConnector connector : CONNECTORS) {
            if (connector.canHandle(geocode)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get the connector handling all the operations available on this geocache. There is always a connector, it might
     * be the {@link UnknownConnector} if the geocache can't be identified.
     */
    @NonNull
    public static IConnector getConnector(final Geocache cache) {
        return getConnector(cache.getGeocode());
    }

    /**
     * Get a connector capability for the given geocache. This might be {@code null} if the connector does not support
     * the given capability.
     *
     * @return the connector cast to the requested capability or {@code null}.
     */
    @Nullable
    public static <T extends IConnector> T getConnectorAs(@Nullable final Geocache cache, @NonNull final Class<T> capabilityClass) {
        if (cache == null) {
            return null;
        }
        final IConnector connector = getConnector(cache);
        if (capabilityClass.isInstance(connector)) {
            return capabilityClass.cast(connector);
        }
        return null;
    }

    @NonNull
    public static TrackableConnector getConnector(final Trackable trackable) {
        return getTrackableConnector(trackable.getGeocode());
    }

    @NonNull
    public static TrackableConnector getTrackableConnector(final String geocode) {
        return getTrackableConnector(geocode, TrackableBrand.UNKNOWN);
    }

    @NonNull
    public static TrackableConnector getTrackableConnector(final String geocode, final TrackableBrand brand) {
        for (final TrackableConnector connector : TRACKABLE_CONNECTORS) {
            if (connector.canHandleTrackable(geocode, brand)) {
                return connector;
            }
        }
        return UNKNOWN_TRACKABLE_CONNECTOR; // avoid null checks by returning a non implementing connector
    }

    /**
     * Get the list of active generic trackable connectors
     *
     * @return the list of actives connectors.
     */
    public static List<TrackableConnector> getGenericTrackablesConnectors() {
        final List<TrackableConnector> trackableConnectors = new ArrayList<>();
        for (final TrackableConnector connector : TRACKABLE_CONNECTORS) {
            if (connector.isActive()) {
                trackableConnectors.add(connector);
            }
        }
        return trackableConnectors;
    }

    /**
     * Get the list of active generic trackable connectors with support logging and currently connected
     *
     * @return the list of actives connectors supporting logging.
     */
    public static List<TrackableConnector> getLoggableGenericTrackablesConnectors() {
        final List<TrackableConnector> trackableConnectors = new ArrayList<>();
        for (final TrackableConnector connector : getGenericTrackablesConnectors()) {
            if (connector.isGenericLoggable() && connector.isRegistered()) {
                trackableConnectors.add(connector);
            }
        }
        return trackableConnectors;
    }

    @NonNull
    public static IConnector getConnector(final String geocodeInput) {
        // this may come from user input
        final String geocode = StringUtils.trim(geocodeInput);
        if (geocode == null) {
            return UNKNOWN_CONNECTOR;
        }
        if (isInvalidGeocode(geocode)) {
            return UNKNOWN_CONNECTOR;
        }
        for (final IConnector connector : CONNECTORS) {
            if (connector.canHandle(geocode)) {
                return connector;
            }
        }
        // in case of errors, take UNKNOWN to avoid null checks everywhere
        return UNKNOWN_CONNECTOR;
    }

    /**
     * Obtain the connector by it's name.
     * If connector is not found, return UNKNOWN_CONNECTOR.
     *
     * @param connectorName connector name String
     * @return The connector matching name
     */
    @NonNull
    public static IConnector getConnectorByName(final String connectorName) {
        for (final IConnector connector : CONNECTORS) {
            if (StringUtils.equals(connectorName, connector.getName())) {
                return connector;
            }
        }
        // in case of errors, take UNKNOWN to avoid null checks everywhere
        return UNKNOWN_CONNECTOR;
    }

    private static boolean isInvalidGeocode(final String geocode) {
        return StringUtils.isBlank(geocode) || !Character.isLetterOrDigit(geocode.charAt(0));
    }

    /**
     * @see ISearchByViewPort#searchByViewport
     */
    @NonNull
    public static SearchResult searchByViewport(@NonNull final Viewport viewport) {
        return SearchResult.parallelCombineActive(searchByViewPortConns, connector -> connector.searchByViewport(viewport));
    }

    @Nullable
    public static String getGeocodeFromURL(@Nullable final String url) {
        if (url == null) {
            return null;
        }
        for (final IConnector connector : CONNECTORS) {
            final String geocode = connector.getGeocodeFromUrl(url);
            if (StringUtils.isNotBlank(geocode)) {
                return StringUtils.upperCase(geocode);
            }
        }
        return null;
    }

    @Nullable
    public static String getGeocodeFromText(@Nullable final String text) {
        if (text == null) {
            return null;
        }
        for (final IConnector connector : CONNECTORS) {
            final String geocode = connector.getGeocodeFromText(text);
            if (StringUtils.isNotBlank(geocode)) {
                return StringUtils.upperCase(geocode);
            }
        }
        return null;
    }

    /**
     * Checks if text can be interpreted as a geocode, either directly or by extracting one
     *
     * @param text String containing a geocode (or not)
     * @return true if a geocode is found
     */
    public static boolean containsGeocode(@Nullable final String text) {
        return (getGeocodeFromURL(text) != null || getGeocodeFromText(text) != null);
    }

    @NonNull
    public static Collection<TrackableConnector> getTrackableConnectors() {
        return TRACKABLE_CONNECTORS;
    }

    /**
     * Get trackable geocode from an URL.
     *
     * @return the geocode, {@code null} if the URL cannot be decoded
     */
    @Nullable
    public static String getTrackableFromURL(final String url) {
        if (url == null) {
            return null;
        }
        for (final TrackableConnector connector : TRACKABLE_CONNECTORS) {
            final String geocode = connector.getTrackableCodeFromUrl(url);
            if (StringUtils.isNotBlank(geocode)) {
                return geocode;
            }
        }
        return null;
    }

    /**
     * Get trackable Tracking Code from an URL.
     *
     * @return the TrackableTrackingCode object, {@code null} if the URL cannot be decoded
     */
    @NonNull
    public static TrackableTrackingCode getTrackableTrackingCodeFromURL(final String url) {
        if (url == null) {
            return TrackableTrackingCode.EMPTY;
        }
        for (final TrackableConnector connector : TRACKABLE_CONNECTORS) {
            final String trackableCode = connector.getTrackableTrackingCodeFromUrl(url);
            if (StringUtils.isNotBlank(trackableCode)) {
                return new TrackableTrackingCode(trackableCode, connector.getBrand());
            }
        }
        return TrackableTrackingCode.EMPTY;
    }

    /**
     * Load a trackable.
     *
     * We query all the connectors that can handle the trackable in parallel as well as the local storage.
     * We return the first positive result coming from a connector, or, if none, the result of loading from
     * the local storage.
     *
     * @param geocode trackable geocode
     * @param guid    trackable guid
     * @param id      trackable id
     * @param brand   trackable brand
     * @return The Trackable observable
     */
    public static Maybe<Trackable> loadTrackable(final String geocode, final String guid, final String id, final TrackableBrand brand) {
        if (StringUtils.isEmpty(geocode)) {
            // Only solution is GC search by uid
            return Maybe.fromCallable(() -> TravelBugConnector.getInstance().searchTrackable(geocode, guid, id)).subscribeOn(AndroidRxUtils.networkScheduler);
        }

        final Observable<Trackable> fromNetwork =
                Observable.fromIterable(getTrackableConnectors()).filter(trackableConnector -> trackableConnector.canHandleTrackable(geocode, brand)).flatMapMaybe((Function<TrackableConnector, Maybe<Trackable>>) trackableConnector -> Maybe.fromCallable(() -> trackableConnector.searchTrackable(geocode, guid, id)).subscribeOn(AndroidRxUtils.networkScheduler));

        final Maybe<Trackable> fromLocalStorage = Maybe.fromCallable(() -> DataStore.loadTrackable(geocode)).subscribeOn(Schedulers.io());

        return fromNetwork.firstElement().switchIfEmpty(fromLocalStorage);
    }

    /**
     * Check if cgeo must relog even if already logged in.
     *
     * @return {@code true} if it is necessary to relog
     */
    public static boolean mustRelog() {
        final boolean mustLogin = forceRelog;
        forceRelog = false;
        return mustLogin;
    }

    /**
     * Force cgeo to relog when reaching the main activity.
     */
    public static void forceRelog() {
        forceRelog = true;
    }

}

<MSG> findbugs: prohibit access to internal collections
<DFF> @@ -25,13 +25,15 @@ import cgeo.geocaching.geopoint.Viewport;
 import org.apache.commons.lang3.StringUtils;
 import org.eclipse.jdt.annotation.NonNull;
 
-import java.lang.reflect.Array;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 public final class ConnectorFactory {
     private static final UnknownConnector UNKNOWN_CONNECTOR = new UnknownConnector();
-    private static final IConnector[] CONNECTORS = new IConnector[] {
+    private static final Collection<IConnector> CONNECTORS = Collections.unmodifiableCollection(Arrays.asList(new IConnector[] {
             GCConnector.getInstance(),
             ECConnector.getInstance(),
             new OCApiLiveConnector("opencaching.de", "www.opencaching.de", "OC", "CC BY-NC-ND, alle Logeinträge © jeweiliger Autor",
@@ -53,42 +55,41 @@ public final class ConnectorFactory {
             new GeopeitusConnector(),
             new WaymarkingConnector(),
             UNKNOWN_CONNECTOR // the unknown connector MUST be the last one
-    };
+    }));
 
     @NonNull public static final UnknownTrackableConnector UNKNOWN_TRACKABLE_CONNECTOR = new UnknownTrackableConnector();
-    private static final TrackableConnector[] TRACKABLE_CONNECTORS = new TrackableConnector[] {
+    private static final Collection<TrackableConnector> TRACKABLE_CONNECTORS = Collections.unmodifiableCollection(Arrays.asList(new TrackableConnector[] {
             new GeokretyConnector(), // GK must be first, as it overlaps with the secret codes of travel bugs
             TravelBugConnector.getInstance(),
             UNKNOWN_TRACKABLE_CONNECTOR // must be last
-    };
+    }));
 
-    private static final ISearchByViewPort[] searchByViewPortConns = getMatchingConnectors(ISearchByViewPort.class);
+    private static final Collection<ISearchByViewPort> searchByViewPortConns = getMatchingConnectors(ISearchByViewPort.class);
 
-    private static final ISearchByCenter[] searchByCenterConns = getMatchingConnectors(ISearchByCenter.class);
+    private static final Collection<ISearchByCenter> searchByCenterConns = getMatchingConnectors(ISearchByCenter.class);
 
-    private static final ISearchByKeyword[] searchByKeywordConns = getMatchingConnectors(ISearchByKeyword.class);
+    private static final Collection<ISearchByKeyword> searchByKeywordConns = getMatchingConnectors(ISearchByKeyword.class);
 
     @SuppressWarnings("unchecked")
-    private static <T extends IConnector> T[] getMatchingConnectors(final Class<T> clazz) {
+    private static <T extends IConnector> Collection<T> getMatchingConnectors(final Class<T> clazz) {
         final List<T> matching = new ArrayList<T>();
         for (final IConnector connector : CONNECTORS) {
             if (clazz.isInstance(connector)) {
                 matching.add((T) connector);
             }
         }
-        T[] result = (T[]) Array.newInstance(clazz, matching.size());
-        return matching.toArray(result);
+        return Collections.unmodifiableCollection(matching);
     }
 
-    public static IConnector[] getConnectors() {
+    public static Collection<IConnector> getConnectors() {
         return CONNECTORS;
     }
 
-    public static ISearchByCenter[] getSearchByCenterConnectors() {
+    public static Collection<ISearchByCenter> getSearchByCenterConnectors() {
         return searchByCenterConns;
     }
 
-    public static ISearchByKeyword[] getSearchByKeywordConnectors() {
+    public static Collection<ISearchByKeyword> getSearchByKeywordConnectors() {
         return searchByKeywordConns;
     }
 
@@ -175,7 +176,7 @@ public final class ConnectorFactory {
         return null;
     }
 
-    public static TrackableConnector[] getTrackableConnectors() {
+    public static Collection<TrackableConnector> getTrackableConnectors() {
         return TRACKABLE_CONNECTORS;
     }
 

findbugs: prohibit access to internal collections