public class QueryStore {

    public String name;
    public double lat;
    public double lon;
    public boolean debug;

    // Filters
    // If empty assume match all

    //public CacheTypeList cacheTypeList = new CacheTypeList();
    //public ContainerTypeList containerTypeList  = new ContainerTypeList();
    //public OneToFiveFilter terrainFilter = new OneToFiveFilter();
    //public OneToFiveFilter difficultyFilter = new OneToFiveFilter();

    public QueryStore() {
        //radius = Prefs.getRadius(cxt);
        //cacheTypeList = Prefs.getCacheTypeFilter(cxt);
        //containerTypeList = Prefs.getContainerTypeFilter(cxt);
        //terrainFilter = Prefs.getTerrainFilter(cxt);
        //difficultyFilter = Prefs.getDifficultyFilter(cxt);

        name = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(new Date());
        name = name.replaceAll(":", ".");
        name = name.replaceAll("/", "-");
    }


    public QueryStore(Bundle bundle) {
        name = bundle.getString("QueryStore_name");
        //radius = bundle.getInt("QueryStore_radius");
        lat = bundle.getDouble("QueryStore_lat");
        lon = bundle.getDouble("QueryStore_lon");

        debug = bundle.getBoolean("QueryStore_debug");

        // Restore cookie list from 2 arrays

        //viewStateMap = new HashMap<String,String>();

        //ArrayList<String> names = bundle.getStringArrayList("QueryStore_viewStateMap_names");
        //ArrayList<String> values = bundle.getStringArrayList("QueryStore_viewStateMap_values");

        //for (int i=0; i<names.size(); i++) {
        //    viewStateMap.put(names.get(i), values.get(i));
        //}


//        // Retrieve cache type filter
//        
//        cacheTypeList = new CacheTypeList(bundle.getString("QueryStore_cacheType"));
//        
//        // Retrieve container type filter
//        
//        containerTypeList = new ContainerTypeList(bundle.getString("QueryStore_containerType"));
//
//        difficultyFilter = new OneToFiveFilter(bundle.getString("QueryStore_difficulty"));
//        terrainFilter = new OneToFiveFilter(bundle.getString("QueryStore_terrain"));
    }


    public void saveToBundle(Bundle bundle) {
        bundle.putString("QueryStore_name", name);
        //bundle.putInt("QueryStore_radius", radius);
        bundle.putDouble("QueryStore_lat", lat);
        bundle.putDouble("QueryStore_lon", lon);

        // Store the cookie list into 2 arrays

        //ArrayList<String> names = new ArrayList<String>();
        //ArrayList<String> values = new ArrayList<String>();

        //for (String c: viewStateMap.keySet()) {
        //    names.add(c);
        //    values.add(viewStateMap.get(c));
        //}

        //bundle.putStringArrayList("QueryStore_viewStateMap_names", names);
        //bundle.putStringArrayList("QueryStore_viewStateMap_values", values);


        bundle.putBoolean("QueryStore_debug", debug);

        // Store cache type filter

//        bundle.putString("QueryStore_cacheType", cacheTypeList.toString());
//        
//        // Store container type filter
//        
//        bundle.putString("QueryStore_containerType", containerTypeList.toString());
//        
//        bundle.putString("QueryStore_difficulty", difficultyFilter.toString());
//        bundle.putString("QueryStore_terrain", terrainFilter.toString());
    }

    public boolean haveLocation() {
        if (lat == 0 || lat == 0)
            return false;
        return true;
    }

    public Location getLocation() {
        Location ret = new Location("rob");
        ret.setLatitude(lat);
        ret.setLongitude(lon);
        return ret;
    }
} public class DownloadablePQ implements Parcelable, Serializable, PQListItem {

    private static final long serialVersionUID = -6540850109992510771L;
    public String name;
    public String size;
    public String age;
    public String waypoints;
    public String url;

    public DownloadablePQ(Parcel in) {
        name = in.readString();
        size = in.readString();
        age = in.readString();
        waypoints = in.readString();
        url = in.readString();
    }

    public DownloadablePQ() {
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeString(size);
        dest.writeString(age);
        dest.writeString(waypoints);
        dest.writeString(url);
    }

    /**
     * Implementing the Parcelable interface must also have a static field called CREATOR, which is an object
     * implementing the Parcelable.Creator interface.
     */
    public static final Parcelable.Creator<DownloadablePQ> CREATOR = new Parcelable.Creator<DownloadablePQ>() {
        public DownloadablePQ createFromParcel(Parcel in) {
            return new DownloadablePQ(in);
        }

        public DownloadablePQ[] newArray(int size) {
            return new DownloadablePQ[size];
        }
    };

    @Override
    public String getName() {
        return name;
    }

}
 public enum CacheType {

    TRADITIONAL(R.string.type_traditional),
    MULTI(R.string.type_multi),
    VIRTUAL(R.string.type_virtual),
    LETTERBOX_HYBRID(R.string.type_letterbox),
    EVENT(R.string.type_event),
    UNKNOWN(R.string.type_unknown),
    APE(R.string.type_project),
    WEBCAM(R.string.type_webcam),
    EARTHCACHE(R.string.type_earthcache),
    ADVENTURES(R.string.type_adventures),
    WHERIGO(R.string.type_wherigo);

    private int resId;

    CacheType(int resId) {
        this.resId = resId;
    }

    public int getResourceId() {
        return resId;
    }

} public class CacheTypeList implements Iterable<CacheType> {

    /**
     * wrapped list
     */
    private LinkedList<CacheType> inner = new LinkedList<CacheType>();

    public CacheTypeList() {
    }


    /**
     * Construct from a string of comma separated string
     * Used to retrieve from preferences
     */
    public CacheTypeList(String s) {
        Asserts.checkNotNull(s);

        String[] cacheList = s.split(",");

        for (String cache : cacheList) {
            if (cache.length() > 0)
                inner.add(CacheType.valueOf(cache));
        }

        if (inner.size() == 0) {
            setAll();
        }
    }

    /**
     * Create list from an array
     * Expect array to contain a boolean for each CacheType
     */
    public CacheTypeList(boolean[] selection) {

        Assert.assertEquals(CacheType.values().length, selection.length);

        for (int i = 0; i < selection.length; i++) {

            if (selection[i]) {
                inner.add(CacheType.values()[i]);
            }
        }

        if (CacheType.values().length == inner.size())
            inner.clear();

    }

    /**
     * Convert to comma seperated string. Uses enum type name
     */
    public String toString() {
        String ret = "";
        for (CacheType cache : inner) {
            ret += cache.toString() + ",";
        }
        return ret;
    }

    /**
     * A nice, comma separated, localized, HTML list for presentation to user
     * Enum value converted into display string
     */
    public String toLocalisedString(Resources res) {

        if (isAll())
            return "<font color='" + MyColors.LIME + "'>" + res.getString(R.string.any) + "</font>";

        StringBuffer ret = new StringBuffer();
        ret.append("<font color='" + MyColors.MEGENTA + "'>");

        for (CacheType cache : inner) {
            ret.append(res.getString(cache.getResourceId()) + ", ");
        }
        // Knock off ending ', '
        if (ret.length() > 0)
            ret.setLength(ret.length() - 2);

        ret.append("</font>");
        return ret.toString();
    }

    public boolean add(CacheType cache) {
        return inner.add(cache);
    }

    public void clear() {
        inner.clear();
    }

    public void setAll() {
        inner.clear();
    }

    /**
     * Does list contain all CacheTypes
     * Using an empty list as a shortcut to represent all
     */
    public boolean isAll() {
        if (inner.size() == 0)
            return true;
        return false;
    }

    @Override
    public Iterator<CacheType> iterator() {
        return inner.iterator();
    }

    public boolean[] getAsBooleanArray() {
        boolean[] ret = new boolean[CacheType.values().length];

        if (isAll()) {
            for (int i = 0; i < ret.length; i++) {
                ret[i] = true;
            }
            return ret;
        }

        for (int i = 0; i < CacheType.values().length; i++) {
            if (inner.contains(CacheType.values()[i]))
                ret[i] = true;
        }

        return ret;
    }
} public class CheckBoxesFilter {

    public boolean enabled;
    public boolean notOnIgnore;
    public boolean notFound;
    public boolean found7days;
    public boolean travelBug;
    public boolean idontown;
    public boolean notBeenFound;


    public CheckBoxesFilter() {
    }

    public CheckBoxesFilter(boolean[] selections) {
        enabled = selections[0];
        notOnIgnore = selections[1];
        notFound = selections[2];
        found7days = selections[3];
        travelBug = selections[4];
        idontown = selections[5];
        notBeenFound = selections[6];
    }

    public String toLocalisedString(Resources res) {
        StringBuffer ret = new StringBuffer();

        if (enabled)
            ret.append(res.getString(R.string.filter_enabled) + ", ");
        if (notOnIgnore)
            ret.append(res.getString(R.string.filter_ignorelist) + ", ");
        if (notFound)
            ret.append(res.getString(R.string.filter_notfound) + ", ");
        if (found7days)
            ret.append(res.getString(R.string.filter_found7days) + ", ");
        if (travelBug)
            ret.append(res.getString(R.string.filter_travelbug) + ", ");
        if (idontown)
            ret.append(res.getString(R.string.filter_idontown) + ", ");
        if (notBeenFound)
            ret.append(res.getString(R.string.filter_notbeenfound) + ", ");
        // Knock off ending ', '
        if (ret.length() > 0)
            ret.setLength(ret.length() - 2);

        return ret.toString();
    }

    public String[] getOptions(Resources res) {
        String[] ret = new String[7];
        ret[0] = res.getString(R.string.filter_enabled);
        ret[1] = res.getString(R.string.filter_ignorelist);
        ret[2] = res.getString(R.string.filter_notfound);
        ret[3] = res.getString(R.string.filter_found7days);
        ret[4] = res.getString(R.string.filter_travelbug);
        ret[5] = res.getString(R.string.filter_idontown);
        ret[6] = res.getString(R.string.filter_notbeenfound);
        return ret;
    }

    public boolean[] getAsBooleanArray() {
        boolean[] ret = new boolean[7];
        ret[0] = enabled;
        ret[1] = notOnIgnore;
        ret[2] = notFound;
        ret[3] = found7days;
        ret[4] = travelBug;
        ret[5] = idontown;
        ret[6] = notBeenFound;
        return ret;
    }
}
 public enum ContainerType {

    SMALL(R.string.container_small),
    OTHER(R.string.container_other),
    VIRTUAL(R.string.container_virtual),
    LARGE(R.string.container_large),
    REGULAR(R.string.container_regular),
    MICRO(R.string.container_micro),
    UNKNOWN(R.string.container_unknown);


    private int resId;

    ContainerType(int resId) {
        this.resId = resId;
    }

    public int getResourceId() {
        return resId;
    }
} public class ContainerTypeList implements Iterable<ContainerType> {

    private LinkedList<ContainerType> inner = new LinkedList<ContainerType>();

    public ContainerTypeList() {
    }

    public ContainerTypeList(String s) {
        Assert.assertNotNull(s);

        String[] cacheList = s.split(",");

        for (String cache : cacheList) {
            if (cache.length() > 0)
                inner.add(ContainerType.valueOf(cache));
        }

        if (inner.size() == 0) {
            setAll();
        }
    }

    public ContainerTypeList(boolean[] selection) {

        Assert.assertEquals(ContainerType.values().length, selection.length);

        for (int i = 0; i < selection.length; i++) {

            if (selection[i]) {
                inner.add(ContainerType.values()[i]);
            }
        }

        if (ContainerType.values().length == inner.size())
            inner.clear();
    }


    public String toString() {
        String ret = "";
        for (ContainerType cache : inner) {
            ret += cache.toString() + ",";
        }
        return ret;
    }

    /**
     * Get a nice, comma seperated, localized, HTML display string representation of enum
     */
    public String toLocalisedString(Resources res) {
        if (isAll())
            return "<font color='" + MyColors.LIME + "'>" + res.getString(R.string.any) + "</font>";

        StringBuffer ret = new StringBuffer();
        if (isAll())
            ret.append("<font color='" + MyColors.LIME + "'>");
        else
            ret.append("<font color='" + MyColors.MEGENTA + "'>");

        for (ContainerType container : inner) {
            ret.append(res.getString(container.getResourceId()));
            ret.append(", ");
        }
        // Knock off ending ', '
        if (ret.length() > 0)
            ret.setLength(ret.length() - 2);

        ret.append("</font>");
        return ret.toString();
    }

    public boolean add(ContainerType container) {
        return inner.add(container);
    }

    public void clear() {
        inner.clear();
    }

    public void setAll() {
        inner.clear();
    }

    public boolean isAll() {
        if (inner.size() == 0)
            return true;
        return false;
    }

    @Override
    public Iterator<ContainerType> iterator() {
        return inner.iterator();
    }

    public boolean[] getAsBooleanArray() {
        boolean[] ret = new boolean[ContainerType.values().length];

        if (isAll()) {
            for (int i = 0; i < ret.length; i++) {
                ret[i] = true;
            }
            return ret;
        }

        for (int i = 0; i < ContainerType.values().length; i++) {
            if (inner.contains(ContainerType.values()[i]))
                ret[i] = true;
        }

        return ret;
    }
} public class Country implements Comparable<Country> {

    private String name;    /* localized name */
    private String code;    /* geocaching country code? */
    private int flag;       /* image resource id */

    public Country(String name, String code, int flag) {
        this.name = name;
        this.code = code;
        this.flag = flag;
    }

    public String getName() {
        return name;
    }

    public int getFlag() {
        return flag;
    }

    public String getCode() {
        return code;
    }

    /**
     * Base on country code only
     */
    @Override
    public boolean equals(Object other) {
        if(other == null)
            return false;
        if(!(other instanceof Country))
            return false;

        Country otherCountry = (Country) other;
        return this.code.equals(otherCountry.code);
    }

    /**
     * Base on country code onlu
     */
    @Override
    public int hashCode() {
        return this.code.hashCode();
    }

    /**
     * Used to alphabetically sort. Base on the localized country name
     */
    @Override
    public int compareTo(Country another) {
        return name.compareTo(another.name);
    }
} public class CountryList implements Iterable<Country> {

    /**
     * The 'inner' list that this class wraps
     */
    private List<Country> inner = new ArrayList<Country>();

    /**
     * How many countries are there
     */
    private int maxCountries;


    /**
     * Create list containing ALL countries
     *
     * @param cxt
     */
    public CountryList(Context cxt) {

        String [] allCountryNames = cxt.getResources().getStringArray(R.array.country_names);
        String [] allCountryCodes = cxt.getResources().getStringArray(R.array.country_codes);
        TypedArray allCountryFlags = cxt.getResources().obtainTypedArray(R.array.country_flags);

        for (int i=0; i<allCountryNames.length; i++) {
            //Drawable flagDrawable = cxt.getResources().getDrawable(allCountryFlags.getResourceId(i, -1));
            inner.add(new Country(allCountryNames[i], allCountryCodes[i], allCountryFlags.getResourceId(i, -1)));
        }

        maxCountries = allCountryNames.length;

        Collections.sort(inner);
    }

    /**
     * Create list of countries from a comma separated encoded string (as stored in prefs)
     *
     * @param cxt
     * @param s comma separated country code list. If null assume ALL countries. If empty assume NO countries
     */
    public CountryList(Context cxt, String s) {        // no countries;

        String [] allCountryNames = cxt.getResources().getStringArray(R.array.country_names);
        String [] allCountryCodes = cxt.getResources().getStringArray(R.array.country_codes);
        TypedArray allCountryFlags = cxt.getResources().obtainTypedArray(R.array.country_flags);

        maxCountries = allCountryNames.length;


        if (s == null)
            s = "";     // means ALL countries
        else if (s.length() == 0)
            return;     // no countries, so don't proceed;

        List countyCodes = Arrays.asList(s.split(","));



        for (int i=0; i<allCountryNames.length; i++) {

            // s.length() picks up the ALL countries case
            if (s.length() == 0 || countyCodes.contains(allCountryCodes[i])) {
                //Drawable flagDrawable = cxt.getResources().getDrawable(allCountryFlags.getResourceId(i, -1));
                inner.add(new Country(allCountryNames[i], allCountryCodes[i], allCountryFlags.getResourceId(i, -1)));
            }
        }

        Collections.sort(inner);
    }

    /**
     * Encode the country list into a comma separated country code string
     * If ALL countries are selected then 'null' is returned
     *
     * @return encoded string or null
     */
    public String toString() {
        if (isAll())
            return null;                             // all countries

        String ret = "";
        for (Country country : inner) {
            ret += country.getCode() + ",";
        }
        return ret;
    }

    public String toLocalisedString(Resources res) {
        if (isAll())
            return "<font color='" + MyColors.LIME + "'>" + res.getString(R.string.any) + "</font>";
        if (isNone())
            return "<font color='red'>" + res.getString(R.string.none) + "</font>";

        StringBuffer ret = new StringBuffer();
        ret.append("<font color='" + MyColors.MEGENTA + "'>");

        for (Country country : inner) {
            ret.append(country.getName());
            ret.append(", ");
        }
        // Knock off ending ', '
        if (ret.length() > 0)
            ret.setLength(ret.length() - 2);

        ret.append("</font>");
        return ret.toString();
    }

    public boolean isNone() {
        return inner.size() == 0;
    }

    public boolean isAll() {
        return inner.size() == maxCountries;
    }

    /*
    public boolean add(ContainerType container) {
        return inner.add(container);
    }

    public void clear() {
        inner.clear();
    }

    public void setAll() {
        inner.clear();
    }

    public boolean isAll() {
        if (inner.size() == 0)
            return true;
        return false;
    }
    */

    @Override
    public Iterator<Country> iterator() {
        return inner.iterator();
    }

    public Country get(int position) {
        return inner.get(position);
    }

    public boolean contains(Country country) {
        return inner.contains(country);
    }

    public List<Country> getInner() {
        return inner;
    }

    public void remove(Country clickedCountry) {
        inner.remove(clickedCountry);
        Collections.sort(inner);
    }

    public void add(Country clickedCountry) {
        inner.add(clickedCountry);
        Collections.sort(inner);
    }

    public void clear() { inner.clear(); };
} public class DaysToGenerateFilter {

    public boolean[] dayOfWeek = new boolean[7];

    public final static int UNCHECK_DAY_AFTER_QUERY = 0;
    public final static int RUN_EVERY_WEEK_ON_CHECKED_DAYS = 1;
    public final static int RUN_ONCE_THEN_DELETE = 2;

    public int howOftenRun;

    /**
     * Recreate from the string we get back from previously storing it in preferences
     */
    public DaysToGenerateFilter(String serializedString) {
        if (serializedString != null) {
            try {
                deSerializeIntoString(serializedString);
                return;

            } catch (IOException e) {
                Logger.e("Unable to restore days_to_generate filter", e);
            }
        }

        // There was no saved state, or failed to restore state so
        // setup the initial state of this filter
        howOftenRun = RUN_ONCE_THEN_DELETE;
        for (int i=0; i<=6; i++) {
            dayOfWeek[i] = true;
        }
    }

    /**
     * Convert object into a string such that it can be stored easily in preferences
     */
    public String serializeIntoString() {
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            DataOutputStream dos = new DataOutputStream(os);

            dos.writeInt(howOftenRun);

            for (int i = 0; i < 7; i++) {
                dos.writeBoolean(dayOfWeek[i]);
            }

            return new String(os.toString());

        } catch (IOException e) {
            Logger.e("Unable to serialize days to generate filter", e);
        }
        return null;
    }

    private void deSerializeIntoString(String in) throws IOException {
        ByteArrayInputStream os = new ByteArrayInputStream(in.getBytes());
        DataInputStream dos = new DataInputStream(os);

        howOftenRun = dos.readInt();

        for (int i=0; i<7; i++) {
            dayOfWeek[i] = dos.readBoolean();
        }
    }

    /**
     * Try to create a human description of this filter to shown as summary in filter list
     */
    public String toLocalisedString(Resources res) {

        String ret = "";

        // Start with general strategy

        if (howOftenRun == RUN_ONCE_THEN_DELETE) {
            ret += res.getString(R.string.daystogenerate_runqueryonce);
        }

        if (howOftenRun == UNCHECK_DAY_AFTER_QUERY)
            ret += res.getString(R.string.daystogenerate_uncheckDayOfWeekAfterQuery);

        if (howOftenRun == RUN_EVERY_WEEK_ON_CHECKED_DAYS)
            ret += res.getString(R.string.daystogenerate_runQueryEveryWeekOnCheckedDays);


        // Add short list of selected days

        List days = new ArrayList<String>();
        String [] shortWeekDayNames = res.getStringArray(R.array.shortWeekdayNames);

        for (int i=0; i<=6; i++) {
            if (dayOfWeek[i])
                days.add(shortWeekDayNames[i]);
        }

        // If "Run once" is being used, and all days selected, then don't bother adding the days
        // This is the most common used by 99.9% of people
        if (howOftenRun == RUN_ONCE_THEN_DELETE && days.size() == 7)
            return ret;

        ret += " (" + TextUtils.join(", ", days.toArray()) + ")";

        return ret;
    }

} public class OneToFiveFilter {

    public int value;
    public boolean up;

    public OneToFiveFilter() {
        value = 5;
        up = false;
    }

    public OneToFiveFilter(String s) {
        if (s.equals("1")) {
            value = 1;
            up = false;
        } else if (s.equals("5")) {
            value = 5;
            up = true;
        } else if (s.equals("All")) {
            value = 1;
            up = true;
        } else {
            String[] a = s.split(" - ");
            if (a == null)
                Assert.fail();

            int v1 = Integer.parseInt(a[0]);
            int v2 = Integer.parseInt(a[1]);

            if (v1 == 1) {
                up = false;
                value = v2;
            } else if (v2 == 5) {
                up = true;
                value = v1;
            } else
                Assert.fail();

        }
    }

    @Override
    public String toString() {
        if (value == 5 && up)
            return "5";
        if (value == 1 && !up)
            return "1";

        if ((value == 1 && up) || (value == 5 && !up))
            return "All";

        if (up)
            return "" + value + " - 5";
        return "1 - " + value;
    }

    /**
     * Display a colorized summary of this filter
     * @param cxt
     * @return html
     */
    public String toLocalisedString(Context cxt) {
        String ret = "";
        if (isAll())
            ret ="<font color='" + MyColors.LIME + "'>";      // Lime
        else
            ret ="<font color='"  + MyColors.MEGENTA + "'>";      // Magenta

        ret += toString();
        ret += "</font>";
        return ret;
    }

    public boolean isAll() {
        if (value == 1 && up || value == 5 && !up)
            return true;
        return false;
    }
}
 public class Assert {

    public static void assertNotNull(Object a) {
        if (a == null) {
            throw new IllegalArgumentException("null reference");
        }
    }

    public static void assertEquals(int a, int b) {
        if (a != b) {
            throw new IllegalArgumentException("not equal");
        }
    }

    public static void fail() {
        throw new IllegalArgumentException("fail");
    }

    public static void assertTrue(boolean a) {
        if (!a) {
            throw new IllegalArgumentException("not true");
        }
    }

    public static void assertFalse(boolean a) {
        if (a) {
            throw new IllegalArgumentException("true");
        }
    }

    public static void assertNull(Object a) {
        if (a != null) {
            throw new IllegalArgumentException("not null");
        }
    }
} public class GPS {

    public static String getLocality(Context cxt, double lat, double lon) {

        String provider = Prefs.getGeocoderProvider(cxt);
        String providerList[] = cxt.getResources().getStringArray(R.array.geocoder_provider_values);

        List<Address> addresses = null;

        if (provider.equals(providerList[0])) {
            addresses = getLocalityGoogle(cxt, lat, lon);
        } else if (provider.equals(providerList[1])) {
            addresses = getLocalityGisgraphy(cxt, lat, lon);
        } else if (provider.equals(providerList[2])) {
            addresses = getLocalityGeoNames(cxt, lat, lon);
        } else {
            Logger.e("Unknown geocoder provider [" + provider + "]");
        }

        if (addresses != null && addresses.size() > 0) {
            Address address = addresses.get(0);
            StringBuffer ret = new StringBuffer();
            if (address.getAddressLine(0) != null)
                ret.append(address.getAddressLine(0));
            if (address.getLocality() != null) {
                if (ret.length() == 0)
                    ret.append(address.getLocality());
                else
                    ret.append("," + address.getLocality());
            }
            if (ret.length() != 0)
                return ret.toString();
        }
        return null;
    }

    private static List<Address> getLocalityGeoNames(Context cxt, double lat, double lon) {

        Locale locale = Locale.getDefault();
        GeoNamesGeocoder geonamesGeocoder = new GeoNamesGeocoder(cxt, locale);

        try {
            return geonamesGeocoder.getFromLocation(lat, lon, 1);
        } catch (IOException e) {
            Logger.e("failed", e);
        }
        return null;
    }

    private static List<Address> getLocalityGoogle(Context cxt, double lat, double lon) {

        Geocoder geocoder = new Geocoder(cxt, Locale.ENGLISH);
        try {
            return geocoder.getFromLocation(lat, lon, 1);
        } catch (IOException e) {
            Logger.e("failed", e);
        }

        return null;
    }

    public static List<Address> getLocalityGisgraphy(Context cxt, double lat, double lon) {

        Locale locale = Locale.getDefault();
        GisgraphyGeocoder gisgraphyGeocoder = new GisgraphyGeocoder(cxt, locale);

        try {
            return gisgraphyGeocoder.getFromLocation(lat, lon, 1);
        } catch (IOException e) {
            Logger.e("failed", e);
        }
        return null;
    }

} public class Logger {

    public static final String APPNAME = "PocketQueryCreator";

    public static final String STORE_DIR = "/Android/data/org.pquery/files";
    public static final String LOG_NAME = "log.html";

    private static File file;
    private static FileOutputStream fout;

    private static boolean enabled;
    private static boolean fileOpenOk;

    /**
     * Turn on logging
     * An empty log file is created when first turned on
     */
    public static void setEnable(boolean e) {
        if (enabled == e)
            return;     // no change
        enabled = e;
        if (enabled)
            init();
    }

    public static boolean isEnabled() {
        return enabled;
    }

    public static void d(String message) {
        log(message, Log.DEBUG);
    }

    public static void w(String message) {
        log(message, Log.WARN);
    }

    public static void cookie(String message, List<HttpCookie> cookies) {
        if (enabled) {
            StringBuffer s = new StringBuffer();
            s.append(message + "\n");
            for (HttpCookie cookie : cookies) {
                s.append("name=" + cookie.getName() + ", value=" + cookie.getValue() + "\n");
            }
            log(s.toString(), Log.DEBUG);
        }
    }

    public static void d(List<Pair<String,String>> list) {
        if (enabled) {
            StringBuffer s = new StringBuffer();
            for (Pair nvp : list) {
                // Don't log password into log file
                if (nvp.first.equals("ctl00$tbPassword"))
                    s.append(nvp.first + " = password not logged\n");
                else
                    s.append(nvp.first + " = " + nvp.second + "\n");
            }
            log(s.toString(), Log.DEBUG);
        }
    }

    public static void i(String message) {
        log(message, Log.INFO);
    }

    public static void e(String message) {
        log(message, Log.ERROR);
    }

    public static void e(String message, Throwable e) {
        log(message + "\n" + Log.getStackTraceString(e), Log.ERROR);
    }

    /**
     * Log to LogCat and attempt to log to external file (if open)
     */
    private static void log(String message, int level) {
        if (enabled) {
            StackTraceElement caller = Thread.currentThread().getStackTrace()[4];

            String[] classNameSplit = caller.getClassName().split("\\.");
            String className = classNameSplit[classNameSplit.length - 1];

            String location = className + ":" + caller.getMethodName() + "(" + caller.getLineNumber() + ")";

            Log.println(level, location, message);

            if (fileOpenOk) {
                try {
                    Time now = new Time();
                    now.setToNow();
                    StringBuffer out = new StringBuffer();
                    out.append("<font color=silver>" + now.format("%H:%M:%S") + "</font> ");
                    switch (level) {
                        case Log.DEBUG:
                            out.append("<font color=navy>DEBUG</font> ");
                            break;
                        case Log.INFO:
                            out.append("<font color=forestgreen>INFO</font> ");
                            break;
                        case Log.ERROR:
                            out.append("<font color=red>ERROR</font> ");
                            break;
                    }
                    out.append("<font color=navy>[" + location + "]</font> - " + replaceNewlineWithBr(message) + "<br>\r\n");
                    fout.write(out.toString().getBytes());
                } catch (IOException e) {
                    // Could happen if sdcard suddenly mounted
                    // When sdcard un-mounted we would need to re-create fout but can't be bothered
                }
            }
        }
    }

    /**
     * Initialise logging to LogCat and to external file. Any existing log file deleted
     * Has to handle no sdcard or sdcard mounted (fileOpenOk will remain false)
     */
    private static void init() {
        fileOpenOk = false;
        try {
            // Check for sdcard
            if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {

                String appdir = getAppDirectory();

                new File(appdir).mkdirs();
                file = new File(appdir, LOG_NAME);

                if (file.exists()) {
                    if (!file.delete())
                        throw new IOException("unable to delete");
                }
                file.createNewFile();
                fout = new FileOutputStream(file);
                fout.write("<html><head><title>PocketQueryCreator log</title></head><body>\r\n".getBytes());
                fileOpenOk = true;
            }
        } catch (IOException e) {
            Log.e(APPNAME, "Unable to create external log file", e);
        }
    }

    public static File getLogFileName() {
        return new File(getAppDirectory(), LOG_NAME);
    }

    private static String getAppDirectory() {
        return Environment.getExternalStorageDirectory().getAbsolutePath() + STORE_DIR;
    }

    /**
     * Replace all \r\n or \n in a string with the html <br>
     *
     * @param s
     * @return
     */
    public static String replaceNewlineWithBr(String s) {
        String out = s.replaceAll("\r\n|\n", "<br>");
        return out;
    }
} public class Prefs {

    public static final String CGEO = "cgeo_preference";
    public static final String MAX_CACHES = "maxcaches_preference";
    public static final String PREFIX = "nameprefix_preference";
    public static final String GEOCODER_PROVIDER = "geocoding_provider_preference";
    public static final String DEFAULT_DOWNLOAD_DIR = "default_download_dir_preference";
    public static final String USER_DOWNLOAD_DIR = "user_download_dir_preference";
    public static final String AUTO_NAME = "autoname_preference";
    public static final String DEBUG_PREFERENCE = "debug_preference";
    private static final String CACHE_FILTER = "cache_type_filter_preference";
    private static final String CONTAINER_FILTER = "container_type_filter_preference";
    private static final String DIFFICULTY_FILTER = "difficulty_filter_preference";
    private static final String TERRAIN_FILTER = "terrain_filter_preference";
    private static final String DAYS_TO_GENERATE_FILTER = "daystogenerate_filter_preference";
    private static final String COUNTRIES_FILTER = "countries_filter_preference";

    private static final String RADUIS = "radius_preference";
    private static final String COOKIES = "cookies_preference";
    public static final String USERNAME = "username_preference";
    public static final String PASSWORD = "password_preference";

    private static final String ENABLED_FILTER = "enabled_filter_preference";
    private static final String TRAVEL_BUG_FILTER = "travel_bug_filter";
    private static final String NOT_IGNORED_FILTER = "not_ignored_filter";
    private static final String FOUND_7DAYS_FILTER = "found_7days_filter";
    private static final String NOT_FOUND_FILTER = "not_found_filter";
    private static final String I_DONT_OWN_FILTER = "i_dont_own_filter";
    private static final String NOT_BEEN_FOUND_FILTER = "not_been_found_filter";

    private static final String LOCATION_ACCURACY = "location_accuracy_preference";

    private static final String COMMA = "\u001F";
    private static final String SEMI_COLON = "\u007F";

    private static final String DPQ_LIST_STATE = "dpq_list_state";
    private static final String RPQ_LIST_STATE = "rpq_list_state";
    private static final String PQ_LIST_STATE_TIMESTAMP = "pq_list_state_time";

    public static String getGeocoderProvider(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getString(GEOCODER_PROVIDER, cxt.getResources().getStringArray(R.array.geocoder_provider_values)[0]);

    }

    public static int getLocationAccuracy(Context cxt) {
        return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(cxt).getString(LOCATION_ACCURACY, "150"));
    }

    public static CheckBoxesFilter getCheckBoxesFilter(Context cxt) {

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(cxt);
        boolean enabled = prefs.getBoolean(ENABLED_FILTER, true);
        boolean travelBug = prefs.getBoolean(TRAVEL_BUG_FILTER, false);
        boolean notOnIgnore = prefs.getBoolean(NOT_IGNORED_FILTER, true);
        boolean found7days = prefs.getBoolean(FOUND_7DAYS_FILTER, false);
        boolean notFound = prefs.getBoolean(NOT_FOUND_FILTER, false);
        boolean idontown = prefs.getBoolean(I_DONT_OWN_FILTER, false);
        boolean notBeenFound = prefs.getBoolean(NOT_BEEN_FOUND_FILTER, false);

        CheckBoxesFilter ret = new CheckBoxesFilter();
        ret.enabled = enabled;
        ret.travelBug = travelBug;
        ret.notOnIgnore = notOnIgnore;
        ret.found7days = found7days;
        ret.notFound = notFound;
        ret.idontown = idontown;
        ret.notBeenFound = notBeenFound;
        return ret;
    }

    public static String getMaxCaches(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getString(MAX_CACHES, "500");
    }

    public static String getDownloadPrefix(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getString(PREFIX, "pocketquery_");
    }

    public static String getUserSpecifiedDownloadDir(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getString(USER_DOWNLOAD_DIR, Util.getDefaultDownloadDirectory(cxt));
    }

    public static boolean isDefaultDownloadDir(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getBoolean(DEFAULT_DOWNLOAD_DIR, true);
    }

    public static boolean getDownload(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getBoolean("download_preference", true);
    }

    public static int getRetryCount(Context cxt) {
        return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(cxt).getString("retry_preference", "3"));
    }

    public static boolean getDebug(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getBoolean(DEBUG_PREFERENCE, false);
    }

    public static String getDefaultRadius(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getString(RADUIS, "5");
    }

    public static boolean getCgeoCompatability(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getBoolean(CGEO, false);
    }

    public static void saveDefaultRadius(Context cxt, String radius) {
        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putString(RADUIS, radius);
        edit.commit();
    }

    public static boolean isAutoName(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getBoolean(AUTO_NAME, false);
    }

    public static boolean isMetric(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getBoolean("metric_preference", false);
    }

    public static ContainerTypeList getContainerTypeFilter(Context cxt) {
        String list = PreferenceManager.getDefaultSharedPreferences(cxt).getString(CONTAINER_FILTER, "");
        return new ContainerTypeList(list);
    }

    public static CacheTypeList getCacheTypeFilter(Context cxt) {
        String list = PreferenceManager.getDefaultSharedPreferences(cxt).getString(CACHE_FILTER, "");
        return new CacheTypeList(list);
    }

    public static OneToFiveFilter getDifficultyFilter(Context cxt) {
        String d = PreferenceManager.getDefaultSharedPreferences(cxt).getString(DIFFICULTY_FILTER, "1 - 5");
        return new OneToFiveFilter(d);
    }

    public static OneToFiveFilter getTerrainFilter(Context cxt) {
        String t = PreferenceManager.getDefaultSharedPreferences(cxt).getString(TERRAIN_FILTER, "1 - 5");
        return new OneToFiveFilter(t);
    }

    public static DaysToGenerateFilter getDaysToGenerateFilter(Context cxt) {
        String t = PreferenceManager.getDefaultSharedPreferences(cxt).getString(DAYS_TO_GENERATE_FILTER, null);
        return new DaysToGenerateFilter(t);
    }

    public static CountryList getCountriesFilter(Context cxt) {
        String encodedCountryCodes = PreferenceManager.getDefaultSharedPreferences(cxt).getString(COUNTRIES_FILTER, null);

        return new CountryList(cxt, encodedCountryCodes);
    }

    /**
     * Save Countries Filter
     * We use JSON to easily pack the int array into a single String
     * @param cxt
     * @param countries
     */
    public static void saveCountriesFilter(Context cxt, CountryList countries) {

        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putString(COUNTRIES_FILTER, countries.toString());     // CountryList will create a comma separated string
        edit.commit();
    }









    public static void saveCacheTypeFilter(Context cxt, CacheTypeList list) {
        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putString(CACHE_FILTER, list.toString());
        edit.commit();
    }

    public static void saveContainerTypeFilter(Context cxt, ContainerTypeList list) {
        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putString(CONTAINER_FILTER, list.toString());
        edit.commit();
    }

    public static void saveDifficultyFilter(Context cxt, OneToFiveFilter d) {
        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putString(DIFFICULTY_FILTER, d.toString());
        edit.commit();
    }

    public static void saveTerrainFilter(Context cxt, OneToFiveFilter t) {
        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putString(TERRAIN_FILTER, t.toString());
        edit.commit();
    }

    public static void saveCookies(Context cxt, Map<String,String> cookies) {

        String s = "";

        // Store the username and password at front of cookie
        // Allows us to detect if username or password is changed. We dump cookie in that case

        s += getUsername(cxt) + COMMA + getPassword(cxt) + SEMI_COLON;

        // Store the cookie list into a string

        for (Map.Entry<String,String> cookie : cookies.entrySet()) {
            s += cookie.getKey() + COMMA + cookie.getValue() + SEMI_COLON;
        }

        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();

        // Not sure what values we are allowed to have in a preferences string, but
        // I kept loosing the preference values on a reboot until I base64'ed the string

        edit.putString(COOKIES, Base64.encodeBytes(s.getBytes()));

        edit.commit();
    }

    public static void clearCookies(Context cxt) {
        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.remove(COOKIES);
        edit.commit();
    }

    public static Map<String,String> getCookies(Context cxt) {
        Map<String,String> ret = new HashMap<>();

        // Decode Base64 wrapper

        String s;
        try {
            s = new String(Base64.decode(PreferenceManager.getDefaultSharedPreferences(cxt).getString(COOKIES, "")));
        } catch (IOException e) {
            Logger.e("base64 problem", e);
            return ret;
        }

        if (s.length() == 0)          // No cookie yet
            return ret;

        // Split seperate cookies by SEMI_COLON, then name/value pair by COMMA

        String cookies[] = s.split(SEMI_COLON);

        for (int i = 0; i < cookies.length; i++) {

            String cookie = cookies[i];
            String parts[] = cookie.split(COMMA);

            if (i == 0) {
                // Username and password are stored in first cookie slot
                // Check match current preferences. If not we have to dump cookie

                if (parts[0].equals(getUsername(cxt)) && parts[1].equals(getPassword(cxt))) {
                    // Match ok
                } else {
                    // Cookie is no longer for the current user
                    clearCookies(cxt);      // Erase what we have
                    return ret;             // Return empty cookie
                }

            } else {

                // Check for bad cookie. Shouldn't happen
                if (parts.length == 2) {
                    ret.put(parts[0], parts[1]);
                } else {
                    Logger.e("got bad cookie [" + cookie + "]");
                }
            }
        }

        return ret;
    }

    public static String getUsername(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getString(USERNAME, "");
    }

    public static String getPassword(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getString(PASSWORD, "");
    }

    public static void saveCheckBoxesFilter(Context cxt, CheckBoxesFilter checkBoxesFilter) {

        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putBoolean(ENABLED_FILTER, checkBoxesFilter.enabled);
        edit.putBoolean(TRAVEL_BUG_FILTER, checkBoxesFilter.travelBug);
        edit.putBoolean(NOT_IGNORED_FILTER, checkBoxesFilter.notOnIgnore);
        edit.putBoolean(FOUND_7DAYS_FILTER, checkBoxesFilter.found7days);
        edit.putBoolean(NOT_FOUND_FILTER, checkBoxesFilter.notFound);
        edit.putBoolean(I_DONT_OWN_FILTER, checkBoxesFilter.idontown);
        edit.putBoolean(NOT_BEEN_FOUND_FILTER, checkBoxesFilter.notBeenFound);
        edit.commit();
    }

    public static void saveAutoName(Context cxt, boolean autoName) {
        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putBoolean(AUTO_NAME, autoName);
        edit.commit();
    }

    public static void saveUserSpecifiedDownloadDir(Context cxt, String dir) {
        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putString(USER_DOWNLOAD_DIR, dir);
        edit.commit();
    }

    public static void savePQListState(Context cxt, DownloadablePQ[] dpqs, RepeatablePQ[] rpqs) {
        String dpqSerialized = null;
        if (dpqs != null) {
            dpqSerialized = new Gson().toJson(dpqs);
        }

        String rpqSerialized = null;
        if (rpqs != null) {
            rpqSerialized = new Gson().toJson(rpqs);
        }

        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putString(DPQ_LIST_STATE, dpqSerialized);
        edit.putString(RPQ_LIST_STATE, rpqSerialized);
        edit.putLong(PQ_LIST_STATE_TIMESTAMP, new Date().getTime());
        edit.commit();
    }


    public static void saveDaysToGenerateFilter(Context cxt, DaysToGenerateFilter f) {
        String ser = f.serializeIntoString();

        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.putString(DAYS_TO_GENERATE_FILTER, ser);
        edit.commit();
    }


    public static long getPQListStateTimestamp(Context cxt) {
        return PreferenceManager.getDefaultSharedPreferences(cxt).getLong(PQ_LIST_STATE_TIMESTAMP, 0);
    }

    public static DownloadablePQ[] getDPQListState(Context cxt) {
        String pqsSerial = PreferenceManager.getDefaultSharedPreferences(cxt).getString(DPQ_LIST_STATE, null);

        if (pqsSerial == null)
            return null;

        if (pqsSerial == "")
            return new DownloadablePQ[0];


        return new Gson().fromJson(pqsSerial, DownloadablePQ[].class);
    }

    public static RepeatablePQ[] getRPQListState(Context cxt) {
        String pqsSerial = PreferenceManager.getDefaultSharedPreferences(cxt).getString(RPQ_LIST_STATE, null);

        if (pqsSerial == null)
            return null;

        if (pqsSerial == "")
            return new RepeatablePQ[0];


        return new Gson().fromJson(pqsSerial, RepeatablePQ[].class);
    }



    public static void userNameChanged(Context cxt) {
        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.remove(DPQ_LIST_STATE);
        edit.remove(RPQ_LIST_STATE);
        edit.putLong(PQ_LIST_STATE_TIMESTAMP, new Date().getTime());
        edit.remove(COOKIES);
        edit.commit();
    }

    public static void erasePQListState(Context cxt) {
        Editor edit = PreferenceManager.getDefaultSharedPreferences(cxt).edit();
        edit.remove(DPQ_LIST_STATE);
        edit.remove(RPQ_LIST_STATE);
        edit.remove(PQ_LIST_STATE_TIMESTAMP);
        edit.commit();
    }
} public interface CancelledListener {
    void ifCancelledThrow() throws InterruptedException;
}
 public class CreateOutputDirectoryTask extends RetriableTask<File> {

    private Context cxt;

    public CreateOutputDirectoryTask(int numberOfRetries, int fromPercent, int toPercent, ProgressListener progressListener, CancelledListener cancelledListener, Context cxt) {
        super(numberOfRetries, fromPercent, toPercent, progressListener, cancelledListener, cxt.getResources());
        this.cxt = cxt;
    }

    @Override
    protected File task() throws FailureException, FailurePermanentException {

        Logger.d("enter");

        // Check for sd-card being mounted
        // If one isn't inserted or it is being accessed by a PC, there isn't
        // much we can do

        if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            throw new FailureException(res.getString(R.string.external_storage_unavailable));
        }

        boolean error = false;

        // Get the output directory
        // This will usually be sdcard/Downloads but the user can
        // override this in preferences

        String dir = Util.getDefaultDownloadDirectory(cxt);
        if (!Prefs.isDefaultDownloadDir(cxt))
            dir = Prefs.getUserSpecifiedDownloadDir(cxt);

        File outputDirectory = new File(dir);

        // Check directory tree exists (even possible that the Downloads directory doesn't exist)

        if (outputDirectory.mkdirs() || outputDirectory.isDirectory()) {
            // everything is ok, directory created or already existed
        } else
            error = true;


        if (error)
            throw new FailureException(res.getString(R.string.file_creation_error), outputDirectory.getAbsolutePath());

        return outputDirectory;
    }

}
 public class DownloadTask extends RetriableTask<File> {

    private Context cxt;
    private String url;
    private File dir;
    private String filename;

    public DownloadTask(int numberOfRetries, int fromPercent, int toPercent, ProgressListener progressListener, CancelledListener cancelledListener, Context cxt, String url, File dir, String filename) {
        super(numberOfRetries, fromPercent, toPercent, progressListener, cancelledListener, cxt.getResources());
        this.cxt = cxt;
        this.url = url;
        this.dir = dir;
        this.filename = filename;
    }

    @Override
    protected File task() throws FailureException, FailurePermanentException, InterruptedException {

        FileDetails pq;

        progressReport(0, res.getString(R.string.downloading), res.getString(R.string.requesting));

        // Get the pocket query creation page
        // and read the response. Need to detect if logged in or no

        try {
            pq = IOUtils.httpGetBytes(cxt, url, cancelledListener, new Listener() {

                @Override
                public void update(int bytesReadSoFar, int expectedLength, int percent0to100) {
                    progressReport(
                            percent0to100,
                            res.getString(R.string.downloading),
                            Util.humanDownloadCounter(bytesReadSoFar, expectedLength));
                }
            });

        } catch (HTTPStatusCodeException e) {
            // When DownloadablePQ not run, we get back 302 redirect to <a href="/pocket/">
            if (e.code == HttpURLConnection.HTTP_MOVED_TEMP && e.body.indexOf("<a href=\"/pocket/\">") != -1)
                throw new FailureException(res.getString(R.string.download_not_ready));

            // Treat any other status code as error
            throw new FailureException(res.getString(R.string.download_failed), e);

        } catch (IOException e) {
            throw new FailureException(res.getString(R.string.download_failed), e);
        }

        // Write to output file

        File output = null;

        try {
            Logger.d("Going to write to file");

            // Try to get a unique output file in the given directory with given name
            // If there is a clash, it adds (1) onto the end etc.

            // HACK!
            if (Prefs.getCgeoCompatability(cxt))
                output = Util.getUniqueFile(dir.toString(), pq.filename);
            else
                output = Util.getUniqueFile(dir.toString(), filename);

            FileOutputStream fout = new FileOutputStream(output);

            fout.write(pq.contents);
            fout.close();
            Logger.d("Written to file ok");

        } catch (IOException e) {
            throw new FailurePermanentException(res.getString(R.string.unable_to_write_file));
        }

        return output;
    }

}
 public class FailurePermanentException extends Exception {

    private static final long serialVersionUID = 7687740322490670417L;

    public String error;
    public String details;

    public FailurePermanentException(String error) {
        super(error);
        this.error = error;
    }

    public FailurePermanentException(String error, String details) {
        super(error);
        this.error = error;
        this.details = details;
    }

    public FailurePermanentException(String error, Exception details) {
        super(error, details);
        this.error = error;
        this.details = details.getMessage();
    }

    @Override
    public String toString() {
        String ret = error;
        if (details != null)
            ret += " [" + details + "]";
        return ret;
    }

    public String toHTML() {
        String ret = error;
        if (details != null)
            ret += "<font color=red><small><br>[" + details + "]</small></font>";
        return ret;
    }

}
 public class ProgressInfo {

    public int percent;
    public String htmlMessage;


    public static ProgressInfo create(int percent, String plainMessage, String goodDetail) {
        if (goodDetail != null && goodDetail.length() > 0)
            return new ProgressInfo(percent, plainMessage + " [" + goodDetail + "]");
        else
            return new ProgressInfo(percent, plainMessage);
    }

    public ProgressInfo(int percent, String htmlMessage) {
        this.percent = percent;
        this.htmlMessage = htmlMessage;
    }

    @Override
    public String toString() {
        return "[percent=" + percent + ",htmlMessage=" + htmlMessage + "]";
    }

    public ProgressInfo(Bundle bundle) {
        percent = bundle.getInt("percent");
        htmlMessage = bundle.getString("htmlMessage");
    }

    public void saveToBundle(Bundle bundle) {
        bundle.putInt("percent", percent);
        bundle.putString("htmlMessage", htmlMessage);
    }
}
 public interface ProgressListener {
    public void progressReport(ProgressInfo progressInfo);
}
 public class RetrievePageTask extends RetriableTask<Source> {

    private Context cxt;
    private String urlPath;

    public RetrievePageTask(int numberOfRetries, int fromPercent, int toPercent, ProgressListener progressListener, CancelledListener cancelledListener, Context cxt, String urlPath) {
        super(numberOfRetries, fromPercent, toPercent, progressListener, cancelledListener, cxt.getResources());
        this.cxt = cxt;
        this.urlPath = urlPath;
    }

    @Override
    protected Source task() throws FailureException, FailurePermanentException, InterruptedException {

        Logger.d("enter");

        // Get preferences

        String username = Prefs.getUsername(cxt);
        String password = Prefs.getPassword(cxt);

        String html = "";

        try {
            // Initialize to 0%

            progressReport(0, res.getString(R.string.retrieving_page), res.getString(R.string.requesting));



            //
            // Get the target page
            // Not sure if logged in yet, but try and get it any way. If not logged in it should
            // contain the login form anyway, which is handy

            // 0 - 50%

            try {

                    html = IOUtils.httpGet(cxt, urlPath, cancelledListener, new Listener() {

                        @Override
                        public void update(int bytesReadSoFar, int expectedLength, int percent0to100) {
                            progressReport(
                                    percent0to100 / 2,    // convert to 0-50%
                                    res.getString(R.string.retrieve_page),
                                    Util.humanDownloadCounter(bytesReadSoFar, expectedLength));
                        }
                    });


            } catch (IOException e) {
                Logger.e("Exception downloading login page", e);
                throw new FailureException(res.getString(R.string.login_download_fail), e);
            }

            // Parse the response
            // Check for some common problems and detect if already logged in or not
            // Can take a long time an old CPU but good way to update progress

            // 70%

            progressReport(70, res.getString(R.string.parsing), "");
            GeocachingPage pageParser = new GeocachingPage(html);

            ifCancelledThrow();


            // Check for a completely wrong page returned that doesn't mention
            // Geocaching in the title
            // Likely to be a wifi login page

            if (!pageParser.isGeocachingPage())
                throw new FailurePermanentException(res.getString(R.string.not_geocaching_page));


            // Check the response. Detecting login and premium state
            if (pageParser.isLoggedIn()) {
                if (!pageParser.isPremium())
                    throw new FailurePermanentException(res.getString(R.string.not_premium));

                // This is Good. User already logged in and proper
                // page retrieved. Cookies must be good

                Logger.d("Detected already logged in");
                return pageParser.parsedHtml;
            }







            //
            // So we now need to GET the login form and POST the credentials
            //
            // - extract form values from the previous page
            // - fill in text boxes with username/password
            // - cookies from original request will be used

            // Fill in the form values

            // 0% - 50%

            try {
                html = IOUtils.httpGet(cxt, "/account/signin?returnUrl=" + URLEncoder.encode(urlPath), cancelledListener, new Listener() {

                    @Override
                    public void update(int bytesReadSoFar, int expectedLength, int percent0to100) {
                        progressReport(
                                percent0to100 / 2,
                                res.getString(R.string.login_geocaching_com),
                                Util.humanDownloadCounter(bytesReadSoFar, expectedLength)); // 18-30%
                    }
                });
            } catch (IOException e) {
                throw new FailureException(res.getString(R.string.unable_to_get_login), e);
            }
            pageParser = new GeocachingPage(html);
            FormFields loginForm = pageParser.extractLoginForm();

            FormFieldsExtra loginFormExtra = new FormFieldsExtra(loginForm);
            try {
                loginFormExtra.setValueChecked("Username", username);
                loginFormExtra.setValueChecked("Password", password);

                //loginFormExtra.setValueChecked("ctl00$ContentBody$cbRememberMe", "on");
                //loginFormExtra.checkValue("ctl00$ContentBody$btnSignIn");

            } catch (ParseException e) {
                throw new FailurePermanentException(res.getString(R.string.failed_login_form), e.getMessage());
            }

            List<Pair<String,String>> nameValuePairs = loginFormExtra.toNameValuePairs();

            progressReport(0, res.getString(R.string.login_geocaching_com), res.getString(R.string.requesting));

            try {
                html = IOUtils.httpPost(cxt, nameValuePairs, "/account/signin",
                        true, cancelledListener, new Listener() {

                            @Override
                            public void update(int bytesReadSoFar, int expectedLength, int percent0to100) {
                                progressReport(
                                        percent0to100 / 2,
                                        res.getString(R.string.login_geocaching_com),
                                        Util.humanDownloadCounter(bytesReadSoFar, expectedLength)); // 18-30%
                            }
                        });

            } catch (IOException e) {
                throw new FailureException(res.getString(R.string.unable_to_submit_login), e);
            }

            // Parse response to verify we are now logged in
            //

            progressReport(70, res.getString(R.string.parsing), "");

            pageParser = new GeocachingPage(html);
            ifCancelledThrow();

            if (!pageParser.isLoggedIn()) {
                throw new FailurePermanentException(res.getString(R.string.bad_credentials));
            }








            //
            // Retrieve page for a second time
            // We are now logged in, so should be ok
            //


            progressReport(0, res.getString(R.string.retrieving_page), res.getString(R.string.requesting));

            //
            // Get the target page
            // Not sure if logged in yet, but try and get it any way. If not logged in it should
            // contain the login form anyway, which is handy

            // 0 - 50%

            try {

                html = IOUtils.httpGet(cxt, urlPath, cancelledListener, new Listener() {

                    @Override
                    public void update(int bytesReadSoFar, int expectedLength, int percent0to100) {
                        progressReport(
                                percent0to100 / 2,    // convert to 0-50%
                                res.getString(R.string.retrieve_page),
                                Util.humanDownloadCounter(bytesReadSoFar, expectedLength));
                    }
                });


            } catch (IOException e) {
                Logger.e("Exception downloading login page", e);
                throw new FailureException(res.getString(R.string.login_download_fail), e);
            }





            //
            // Parse the response
            // Check for some common problems and detect if already logged in or not
            // Can take a long time an old CPU but good way to update progress

            // 70%

            progressReport(70, res.getString(R.string.parsing), "");
            pageParser = new GeocachingPage(html);

            ifCancelledThrow();


            // Check for a completely wrong page returned that doesn't mention
            // Geocaching in the title
            // Likely to be a wifi login page

            if (!pageParser.isGeocachingPage())
                throw new FailurePermanentException(res.getString(R.string.not_geocaching_page));


            // Check the response. Detecting login and premium state
            if (pageParser.isLoggedIn()) {
                if (!pageParser.isPremium())
                    throw new FailurePermanentException(res.getString(R.string.not_premium));

                // This is Good. User already logged in and proper
                // page retrieved. Cookies must be good

                Logger.d("Detected already logged in");
                return pageParser.parsedHtml;
            }






            throw new FailurePermanentException("Unknown error");


        } catch (ParseException e) {
            throw new FailurePermanentException(res.getString(R.string.error_parsing), e);
        }
    }


}
 public class SubmitFormPageTask extends RetriableTask<String> {

    private Context cxt;
    private String urlPath;
    private List<Pair<String,String>> form;

    public SubmitFormPageTask(List<Pair<String,String>> form, int numberOfRetries, int fromPercent, int toPercent, ProgressListener progressListener, CancelledListener cancelledListener, Context cxt, String urlPath) {
        super(numberOfRetries, fromPercent, toPercent, progressListener, cancelledListener, cxt.getResources());
        this.cxt = cxt;
        this.urlPath = urlPath;
        this.form = form;
    }

    @Override
    protected String task() throws FailureException, FailurePermanentException, InterruptedException {

        Logger.d("enter");

        String html = "";

        // Create client
        // ASSUMING already logged in at this stage

            // Initialize to 0%

            progressReport(0, res.getString(R.string.creating), res.getString(R.string.submitting));

            try {
                html = IOUtils.httpPost(cxt, form, urlPath, false, cancelledListener, new Listener() {

                    @Override
                    public void update(int bytesReadSoFar, int expectedLength, int percent0to100) {
                        progressReport(
                                percent0to100,
                                res.getString(R.string.creating),
                                Util.humanDownloadCounter(bytesReadSoFar, expectedLength));
                    }
                });


            } catch (IOException e) {
                throw new FailureException(res.getString(R.string.unable_to_submit_creation_form), e);
            }

            return html;

    }


}
 public class FormFieldsExtra {

    private FormFields form;

    public FormFieldsExtra(FormFields wrappedForm) {
        this.form = wrappedForm;
    }

    /**
     * Enable a checkbox. Verifies that everything is as expected. Expects that checkbox
     * will have a value="xx" that will be used when enabled
     *
     * @param name name of checkbox
     * @throws ParseException
     */
    public void enableCheckbox(String name) throws ParseException {

        if (form.get(name) == null)
            throw new ParseException(name + " missing");

        FormControl control = form.get(name).getFormControl();

        if (control.getFormControlType() != FormControlType.CHECKBOX)
            throw new ParseException(name + " not a checkbox");

        if (control.getPredefinedValue() == null)
            throw new ParseException(name + " checkbox has no value");

        boolean success = control.setValue(control.getPredefinedValue());

        if (!success)
            throw new ParseException(name);
    }

    public void setValueChecked(String name, String value) throws ParseException {

        if (form.get(name) == null)
            throw new ParseException(name + " missing");

        boolean success = form.setValue(name, value);

        if (!success)
            throw new ParseException(name);
    }

    /**
     * Check if something exists in the form
     * @param name html 'name'
     * @throws ParseException
     */
    public void checkValue(String name) throws ParseException {
        if (form.get(name) == null)
            throw new ParseException(name + " missing");

        return;
    }

    public void deleteValue(String name) {
        form.remove(form.get(name));
    }

    public List<Pair<String,String>> toNameValuePairs() {

        ArrayList<Pair<String,String>> pairList = new ArrayList<>();

        for (FormField field : form) {

            if (field.getFormControl().getFormControlType() == FormControlType.SUBMIT)
                pairList.add(new Pair(field.getName(), field.getPredefinedValues().iterator().next()));
            else {
                // Usually the field will only have one value
                // However, for a multiple select control, there will be multiple values e.g. country
                for (String value : field.getValues()) {
                    pairList.add(new Pair(field.getName(), value));
                }
            }
        }

        return pairList;
    }

    public void addValue(String name, String value) throws ParseException {
        boolean success = form.addValue(name, value);

        if (!success)
            throw new ParseException(name);
    }

} public class GeocachingPage {

    protected String html;
    public Source parsedHtml;

    public GeocachingPage(String html) {
        this.html = html;
        this.parsedHtml = new Source(html);
    }

    public GeocachingPage(Source parsedHtml) {
        this.parsedHtml = parsedHtml;
    }

    /**
     * Look for the "Down for maintenance" Geocaching web page
     */
    public boolean isDownFormMaintenance() {
        if (html.contains("maintenance"))
            return true;
        return false;
    }

    /**
     * Does the page look like a Geocaching page. Used to detect wifi
     * logon pages etc
     * <p/>
     * All Geocaching.com pages have a title containing 'Geocaching'
     */
    public boolean isGeocachingPage() {
        if (title().contains("Geocaching"))
            return true;
        return false;
    }

    /**
     * Page at http://www.geocaching.com/pocket/default.aspx
     * <p/>
     * In = ctl00_divSignedIn Out = ctl00_divNotSignedIn
     */
    public boolean isLoggedIn() throws ParseException {

        if (html.indexOf("Object moved") != -1) {
            // After a successful login POST to the login.aspx page page is useless and doesn't
            // indicate if there was a successful login
            //
            // However, on success we get back a 302 Object moved
            // The html page also has a "Object Moved" message
            return true;
        }

        if (html.indexOf("ctl00_uxLoginStatus_divNotSignedIn") != -1) {
            return false;
        }

        if (html.indexOf("validation-summary-errors") != -1) {
            return false;       // Your password is incorrect
        }

        if (html.indexOf("ctl00_uxLoginStatus_divSignedIn") != -1) {
            return true;
        }

        throw new ParseException("Unable to detect login status on geocaching.com page");
    }

    public boolean isPremium() throws ParseException {

        // Does this appear when premium membership is coming to an end?
        if (html.contains("account/EditMembership.aspx"))
            return true;
        // Usually this means member is basic and is offered an upgrade link
        if (parsedHtml.getElementById("hlUpgrade") != null)
            return false;

        // At the moment can't find good way to detect premium
        // so assume so (must use along with login state detection)
        return true;
    }

    /**
     * Try to extract title of html page
     */
    public String title() {
        Element titleElement = parsedHtml.getFirstElement(HTMLElementName.TITLE);
        if (titleElement == null)
            return "";

        return CharacterReference.decodeCollapseWhiteSpace(titleElement.getContent());
    }

    /**
     * Extract out form values on page
     */
    public FormFields extractLoginForm() throws ParseException {

        // Do all the work
        Element loginDiv = parsedHtml.getElementById("SignupSignin");

        if (loginDiv == null) {
            throw new ParseException("Can't find login div");
        }

        FormFields formFields = loginDiv.getFormFields();

        return formFields;
    }


    /**
     * Extract out form values on page
     */
    public FormFields extractForm() throws ParseException {

        FormFields formFields = parsedHtml.getFormFields();

        return formFields;
    }

    /**
     * Detect if we appear to be back at the login page
     * This happens is a login attempt fails due to bad credentials
     */
    public boolean atLoginPage() {
        if (html.indexOf("ctl00_ContentBody_LoginPanel") != -1)
            return true;
        return false;
    }
}
 public class ParseException extends Exception {

    private static final long serialVersionUID = -5216388447542802879L;

    public ParseException() {
        super();
    }

    public ParseException(String detail) {
        super(detail);
    }
}
 public class SuccessMessageParser {

    private String successMessage;
    private Resources res;

    /**
     * Try to parse out success message from passed in html
     * <p/>
     * Extracts out everything between <p class="Success"> and </p>
     */
    public SuccessMessageParser(String html, Resources res) throws FailurePermanentException {
        this.res = res;
        final String START = Pattern.quote("<p class=\"Success\">");
        final String END = Pattern.quote("</p>");

        Matcher m = Pattern.compile(START + "(.+)" + END).matcher(html);

        if (m.find()) {
            String name = m.group(1);
            successMessage = name;
        } else
            throw new FailurePermanentException(this.res.getString(R.string.response_indicate_failure));
    }


    /**
     * Extract out the Pocket Query guid (within the preview link)
     * We need it to calculate the download link
     * <p/>
     * Extract between nearest.aspx?pq= and end "
     * <p/>
     * <a href="http://www.geocaching.com/seek/nearest.aspx?pq=692a0d6f-1494-4ba6-9ac9-5877aff7e6b4"
     */
    public String extractDownloadGuid() throws FailurePermanentException {

        final String START = Pattern.quote("nearest.aspx?pq=");
        final String END = Pattern.quote("\"");

        Matcher m = Pattern.compile(START + "(.+?)" + END).matcher(successMessage);

        while (m.find()) {
            String guid = m.group(1);
            return guid;
        }

        throw new FailurePermanentException(res.getString(R.string.parse_error_download_link));
    }

    /**
     * Return number of created Pocket Queries
     * <p/>
     * Matches any digit group before the download link starts
     */
    public int extractNumberPQ() throws FailurePermanentException {

        Matcher m = Pattern.compile("(\\d+).*?<a href").matcher(successMessage);

        while (m.find()) {
            String name = m.group(1);
            return Integer.parseInt(name);
        }

        throw new FailurePermanentException(res.getString(R.string.parse_error_generated_pqs));
    }

    public String toString(Resources res) throws FailurePermanentException {
        return String.format(res.getString(R.string.created_ok_and_matches), extractNumberPQ());
    }

    public String getSuccessMessage() {
        return successMessage;
    }
}

import android.content.Context;
import android.content.res.Resources;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Pair;
import net.htmlparser.jericho.FormFields;
import net.htmlparser.jericho.Source;
import org.pquery.QueryStore;
import org.pquery.R;
import org.pquery.dao.DownloadablePQ;
import org.pquery.filter.CacheType;
import org.pquery.filter.CacheTypeList;
import org.pquery.filter.CheckBoxesFilter;
import org.pquery.filter.ContainerType;
import org.pquery.filter.ContainerTypeList;
import org.pquery.filter.Country;
import org.pquery.filter.CountryList;
import org.pquery.filter.DaysToGenerateFilter;
import org.pquery.filter.OneToFiveFilter;
import org.pquery.util.Assert;
import org.pquery.util.GPS;
import org.pquery.util.Logger;
import org.pquery.util.Prefs;
import org.pquery.webdriver.CancelledListener;
import org.pquery.webdriver.CreateOutputDirectoryTask;
import org.pquery.webdriver.DownloadTask;
import org.pquery.webdriver.FailurePermanentException;
import org.pquery.webdriver.ProgressInfo;
import org.pquery.webdriver.ProgressListener;
import org.pquery.webdriver.RetrievePageTask;
import org.pquery.webdriver.SubmitFormPageTask;
import org.pquery.webdriver.parser.FormFieldsExtra;
import org.pquery.webdriver.parser.GeocachingPage;
import org.pquery.webdriver.parser.ParseException;
import org.pquery.webdriver.parser.SuccessMessageParser;
import java.io.File;
import java.util.List;

package org.pquery.service;





public class CreatePQAsync extends AsyncTask<Void, ProgressInfo, CreatePQResult> implements LocationListener, CancelledListener, ProgressListener {

    private Resources res;
    private Context cxt;

    private QueryStore queryStore;
    private LocationManager locationManager;
    private Location gpsLocation;
    private int retryCount;
    private boolean gpsOn;

    public CreatePQAsync(Context cxt, QueryStore queryStore, LocationManager locationManager) {
        Assert.assertNotNull(queryStore);

        this.res = cxt.getResources();
        this.cxt = cxt;
        this.queryStore = queryStore;
        this.locationManager = locationManager;
        this.retryCount = Prefs.getRetryCount(cxt);
    }

    private void checkCancelled() throws InterruptedException {
        if (isCancelled()) {
            Logger.d("Detected a cancellation. Manually throwing InterruptedException");
            throw new InterruptedException();
        }
    }

    private void startGPS() {
        Logger.d("Turning GPS on");

        // Assume we have already asked for, and possibly received location permission
        // Just swallow any problems

        List<String> providers = locationManager.getAllProviders();
        for (String provider: providers) {
            try {
                locationManager.requestLocationUpdates(provider, 2000, 5, this);
            } catch (SecurityException e) {
                Logger.d("SecurityException; " + e);
            }
        }
        gpsOn = true;
    }

    private void stopGPS() {
        Logger.d("Turning GPS off");
        try {
            locationManager.removeUpdates(this);
        } catch (SecurityException e) {
            Logger.d("failed to remove updates; " + e);
        }
        gpsOn = false;
    }

    /**
     * Called on UI thread before we start
     */
    @Override
    protected void onPreExecute() {
        // Turn on GPS if location not passed in
        // Needs to be done here, on UI thread
        if (!queryStore.haveLocation()) {
            Logger.d("Starting gps");
            publishProgress(new ProgressInfo(1, res.getString((R.string.gps_wait_no_fix))));
            startGPS();
        }
    }

    //    @Override
    //    protected void onPostExecute(ResultInfo result) {
    //        super.onPostExecute(result);
    //        Prefs.saveCookies(cxt, cookies);
    //        Logger.cookie("Saving cookies to prefs", cookies);
    //    }

    @Override
    protected CreatePQResult doInBackground(Void... params) {

        Logger.d("enter");

        try {
            // If GPS on, wait for fix
            // Convert retry count into max minutes to wait for GPS (bit dodgy)

            if (gpsOn) {        // turn on in onPreExecute
                try {
                    if (!waitForGPS(retryCount + 1))

                        return new CreatePQResult(new FailurePermanentException(res.getString(R.string.gps_timeout)));
