<NME> GCVote.java
<BEF> package cgeo.geocaching.gcvote;

import cgeo.geocaching.R;
import cgeo.geocaching.connector.ConnectorFactory;
import cgeo.geocaching.connector.IConnector;
import cgeo.geocaching.connector.capability.ICredentials;
import cgeo.geocaching.connector.capability.IVotingCapability;
import cgeo.geocaching.models.Geocache;
import cgeo.geocaching.network.Network;
import cgeo.geocaching.network.Parameters;
import cgeo.geocaching.settings.Credentials;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.utils.LeastRecentlyUsedMap;
import cgeo.geocaching.utils.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

public final class GCVote implements ICredentials {

    // gcvote.com does not have a https certificate. However, Guido (the owner of gcvote.com) told
    // us on 2017-03-21 that the site is accessible through its provider https endpoint at
    // https://ssl.webpack.de/gcvote.com

    public static final float NO_RATING = 0;

    private static final int MAX_CACHED_RATINGS = 1000;
    private static final LeastRecentlyUsedMap<String, GCVoteRating> RATINGS_CACHE = new LeastRecentlyUsedMap.LruCache<>(MAX_CACHED_RATINGS);

    private GCVote() {
        // utility class
    }

    @NonNull
    public static GCVote getInstance() {
        return SingletonHolder.INSTANCE;
    }

    /**
     * Get user rating for a given guid or geocode. For a guid first the ratings cache is checked
     * before a request to gcvote.com is made.
     */
    @Nullable
    @WorkerThread
    public static GCVoteRating getRating(final String guid, final String geocode) {
        if (StringUtils.isNotBlank(guid) && RATINGS_CACHE.containsKey(guid)) {
            return RATINGS_CACHE.get(guid);
        }

        final Map<String, GCVoteRating> ratings = getRating(singletonOrNull(guid), singletonOrNull(geocode));
        return MapUtils.isNotEmpty(ratings) ? ratings.values().iterator().next() : null;
    }

    @Nullable
    private static List<String> singletonOrNull(final String item) {
        return StringUtils.isNotBlank(item) ? Collections.singletonList(item) : null;
    }

    /**
     * Get user ratings from gcvote.com
     */
    @NonNull
    @WorkerThread
    private static Map<String, GCVoteRating> getRating(final List<String> guids, final List<String> geocodes) {
        if (guids == null && geocodes == null) {
            return Collections.emptyMap();
        }

        final Parameters params = new Parameters("version", "cgeo");
        final Credentials login = Settings.getGCVoteLogin();
        if (login.isValid()) {
            params.put("userName", login.getUserName(), "password", login.getPassword());
        }

        // use guid or gccode for lookup
        final boolean requestByGuids = CollectionUtils.isNotEmpty(guids);
        if (requestByGuids) {
            params.put("cacheIds", StringUtils.join(guids, ','));
        } else {
            params.put("waypoints", StringUtils.join(geocodes, ','));
        }
        final InputStream response = Network.getResponseStream(Network.getRequest("https://ssl.webpack.de/gcvote.com/getVotes.php", params));
        if (response == null) {
            return Collections.emptyMap();
        }
        try {
            return getRatingsFromXMLResponse(response, requestByGuids);
        } finally {
            IOUtils.closeQuietly(response);
        }
    }

    @NonNull
    @WorkerThread
    static Map<String, GCVoteRating> getRatingsFromXMLResponse(@NonNull final InputStream response, final boolean requestByGuids) {
        try {
            final XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
                        }
                    }
                } catch (Exception e) {
                    Log.w(Settings.tag, "cgBase.getRating: Failed to parse guid");
                }
                if (guid == null) {
                    continue;
                    final String tagName = xpp.getName();
                    if (StringUtils.equals(tagName, "vote")) {
                        final String id = xpp.getAttributeValue(null, requestByGuids ? "cacheId" : "waypoint");
                        final float myVote = loggedIn ? Float.parseFloat(xpp.getAttributeValue(null, "voteUser")) : 0;
                        final GCVoteRating voteRating = new GCVoteRating(Float.parseFloat(xpp.getAttributeValue(null, "voteAvg")),
                                Integer.parseInt(xpp.getAttributeValue(null, "voteCnt")),
                                myVote);
                        ratings.put(id, voteRating);
                    } else if (StringUtils.equals(tagName, "votes")) {
                        loggedIn = StringUtils.equals(xpp.getAttributeValue(null, "loggedIn"), "true");
                        }
                    }
                } catch (Exception e) {
                    Log.w(Settings.tag, "cgBase.getRating: Failed to parse loggedIn");
                }

                float rating = 0;
            Log.e("Cannot parse GCVote result", e);
            return Collections.emptyMap();

                        rating = Float.parseFloat(matcherRating.group(1));
                    }
                } catch (Exception e) {
                    Log.w(Settings.tag, "cgBase.getRating: Failed to parse rating");
                }
                if (rating <= 0) {
                    continue;
     * @param rating the rating
     * @return {@code true} if the rating was submitted successfully
     */
    @WorkerThread
    public static boolean setRating(@NonNull final Geocache cache, final float rating) {
        final IConnector connector = ConnectorFactory.getConnector(cache);
                        votes = Integer.parseInt(matcherVotes.group(1));
                    }
                } catch (Exception e) {
                    Log.w(Settings.tag, "cgBase.getRating: Failed to parse vote count");
                }
                if (votes < 0) {
                    continue;
        if (!votingConnector.isValidRating(rating)) {
            throw new IllegalArgumentException("invalid rating " + rating);
        }

        final Credentials login = Settings.getGCVoteLogin();
        if (login.isInvalid()) {
            Log.e("GCVote.setRating: cannot find credentials");
                            myVote = Float.parseFloat(matcherVote.group(1));
                        }
                    } catch (Exception e) {
                        Log.w(Settings.tag, "cgBase.getRating: Failed to parse user's vote");
                    }
                }

                "voteUser", String.format(Locale.US, "%.1f", rating),
                "version", "cgeo");

        final String result = StringUtils.trim(Network.getResponseData(Network.getRequest("https://ssl.webpack.de/gcvote.com/setVote.php", params)));
                }
            }
        } catch (Exception e) {
            Log.e(Settings.tag, "cgBase.getRating: " + e.toString());
        }

        return ratings;
    @WorkerThread
    public static void loadRatings(@NonNull final List<Geocache> caches) {
        if (!Settings.isRatingWanted()) {
            return;
        }

        final List<String> geocodes = getVotableGeocodes(caches);
        if (geocodes.isEmpty()) {
            return;
        }

        try {
            final Map<String, GCVoteRating> ratings = getRating(null, geocodes);

            // save found cache coordinates
            for (final Geocache cache : caches) {
                if (ratings.containsKey(cache.getGeocode())) {
                    final GCVoteRating rating = ratings.get(cache.getGeocode());

                    cache.setRating(rating.getRating());
                    cache.setVotes(rating.getVotes());
                    cache.setMyVote(rating.getMyVote());
                }
            }
        } catch (final Exception e) {
            Log.e("GCVote.loadRatings", e);
        }
    }

    /**
     * Get geocodes of all the caches, which can be used with GCVote. Non-GC caches will be filtered out.
     */
    @NonNull
    private static List<String> getVotableGeocodes(@NonNull final Collection<Geocache> caches) {
        final List<String> geocodes = new ArrayList<>(caches.size());
        for (final Geocache cache : caches) {
            final String geocode = cache.getGeocode();

            final IConnector connector = ConnectorFactory.getConnector(cache);

            if (StringUtils.isNotBlank(geocode) && connector instanceof IVotingCapability && ((IVotingCapability) connector).supportsVoting(cache)) {
                geocodes.add(geocode);
            }
        }
        return geocodes;
    }

    @NonNull
            }
        }

        try {
            final Map<String, GCVoteRating> ratings = GCVote.getRating(guids, null);

    public int getUsernamePreferenceKey() {
        return R.string.pref_user_vote;
    }

    @Override
    public int getPasswordPreferenceKey() {
        return R.string.pref_pass_vote;
    }

    @Override
    public Credentials getCredentials() {
        return Settings.getCredentials(R.string.pref_user_vote, R.string.pref_pass_vote);
    }

    private static class SingletonHolder {
        @NonNull
        private static final GCVote INSTANCE = new GCVote();
    }
}

<MSG> refactoring: cleanups

* remove unused resources
* fix some comments, layout, logging and other stuff
<DFF> @@ -118,7 +118,7 @@ public final class GCVote {
                         }
                     }
                 } catch (Exception e) {
-                    Log.w(Settings.tag, "cgBase.getRating: Failed to parse guid");
+                    Log.w(Settings.tag, "GCVote.getRating: Failed to parse guid");
                 }
                 if (guid == null) {
                     continue;
@@ -135,7 +135,7 @@ public final class GCVote {
                         }
                     }
                 } catch (Exception e) {
-                    Log.w(Settings.tag, "cgBase.getRating: Failed to parse loggedIn");
+                    Log.w(Settings.tag, "GCVote.getRating: Failed to parse loggedIn");
                 }
 
                 float rating = 0;
@@ -145,7 +145,7 @@ public final class GCVote {
                         rating = Float.parseFloat(matcherRating.group(1));
                     }
                 } catch (Exception e) {
-                    Log.w(Settings.tag, "cgBase.getRating: Failed to parse rating");
+                    Log.w(Settings.tag, "GCVote.getRating: Failed to parse rating");
                 }
                 if (rating <= 0) {
                     continue;
@@ -158,7 +158,7 @@ public final class GCVote {
                         votes = Integer.parseInt(matcherVotes.group(1));
                     }
                 } catch (Exception e) {
-                    Log.w(Settings.tag, "cgBase.getRating: Failed to parse vote count");
+                    Log.w(Settings.tag, "GCVote.getRating: Failed to parse vote count");
                 }
                 if (votes < 0) {
                     continue;
@@ -172,7 +172,7 @@ public final class GCVote {
                             myVote = Float.parseFloat(matcherVote.group(1));
                         }
                     } catch (Exception e) {
-                        Log.w(Settings.tag, "cgBase.getRating: Failed to parse user's vote");
+                        Log.w(Settings.tag, "GCVote.getRating: Failed to parse user's vote");
                     }
                 }
 
@@ -183,7 +183,7 @@ public final class GCVote {
                 }
             }
         } catch (Exception e) {
-            Log.e(Settings.tag, "cgBase.getRating: " + e.toString());
+            Log.e(Settings.tag, "GCVote.getRating: " + e.toString());
         }
 
         return ratings;
@@ -238,6 +238,10 @@ public final class GCVote {
             }
         }
 
+        if (guids.isEmpty()) {
+            return;
+        }
+
         try {
             final Map<String, GCVoteRating> ratings = GCVote.getRating(guids, null);
 

refactoring: cleanups