/* * Copyright 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.appsimobile.appsii.module.weather.loader; import android.location.Location; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.v4.util.CircularArray; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.appsimobile.appsii.BuildConfig; import com.appsimobile.appsii.R; import com.appsimobile.appsii.ResponseParserException; import com.appsimobile.appsii.annotation.VisibleForTesting; import com.appsimobile.appsii.module.weather.Utils; import com.appsimobile.util.ArrayUtils; import org.json.JSONException; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URLEncoder; import java.text.ParseException; import java.util.Comparator; import java.util.Locale; /** * Client code for the Yahoo! Weather RSS feeds and GeoPlanet API. */ public class YahooWeatherApiClient { private static final String TAG = "YahooWeatherApiClient"; private static final int MAX_SEARCH_RESULTS = 10; private static final int PARSE_STATE_NONE = 0; private static final int PARSE_STATE_PLACE = 1; private static final int PARSE_STATE_WOEID = 2; private static final int PARSE_STATE_NAME = 3; private static final int PARSE_STATE_COUNTRY = 4; private static final int PARSE_STATE_ADMIN1 = 5; private static final int PARSE_STATE_TIMEZONE = 6; private static final XmlPullParserFactory sXmlPullParserFactory; static { try { sXmlPullParserFactory = XmlPullParserFactory.newInstance(); sXmlPullParserFactory.setNamespaceAware(true); } catch (XmlPullParserException e) { Log.e(TAG, "Could not instantiate XmlPullParserFactory", e); throw new RuntimeException(e); } } @Nullable public static CircularArray<WeatherData> getWeatherForWoeids(CircularArray<String> woeids, String unit) throws CantGetWeatherException { if (woeids == null || woeids.isEmpty()) return null; HttpURLConnection connection = null; try { String url = buildWeatherQueryUrl(woeids, unit); if (BuildConfig.DEBUG) Log.d("YahooWeatherApiClient", "loading weather from: " + url); connection = Utils.openUrlConnection(url); CircularArray<WeatherData> result = new CircularArray<>(); WeatherDataParser.parseWeatherData(result, connection.getInputStream(), woeids); return result; } catch (JSONException | ResponseParserException | ParseException | IOException | NumberFormatException e) { throw new CantGetWeatherException(true, R.string.no_weather_data, "Error parsing weather feed XML.", e); } finally { if (connection != null) { connection.disconnect(); } } } private static String buildWeatherQueryUrl(CircularArray<String> woeids, String unit) { // http://developer.yahoo.com/weather/ String endPoint = "https://query.yahooapis.com/v1/public/yql?format=json&q="; String query = "select * from weather.forecast where woeid in (%s) and u=\"%s\""; String param = ArrayUtils.join(", ", woeids); String queryString = String.format(Locale.ROOT, query, param, unit); if (BuildConfig.DEBUG) Log.d("YahooWeatherApiClient", "yql query: " + queryString); try { queryString = URLEncoder.encode(queryString, "UTF-8"); } catch (UnsupportedEncodingException e) { Log.wtf("YahooWeatherApiClient", "error encoding url", e); } return endPoint + queryString; } public static LocationInfo getLocationInfo(Location location) throws CantGetWeatherException { LocationInfo li = new LocationInfo(); // first=tagname (admin1, locality3) second=woeid HttpURLConnection connection = null; try { connection = Utils.openUrlConnection(buildPlaceSearchUrl(location)); InputStream inputStream = connection.getInputStream(); return parseLocationInfo(li, inputStream); } catch (IOException | XmlPullParserException e) { throw new CantGetWeatherException(true, R.string.no_weather_data, "Error parsing place search XML", e); } finally { if (connection != null) { connection.disconnect(); } } } private static String buildPlaceSearchUrl(Location l) { // GeoPlanet API return "http://where.yahooapis.com/v1/places.q('" + l.getLatitude() + "," + l.getLongitude() + "')" + "?appid=" + YahooWeatherApiConfig.APP_ID; } @VisibleForTesting static LocationInfo parseLocationInfo( LocationInfo li, InputStream in) throws XmlPullParserException, IOException, CantGetWeatherException { CircularArray<Pair<String, String>> alternateWoeids = new CircularArray<>(); String primaryWoeid = null; XmlPullParser xpp = sXmlPullParserFactory.newPullParser(); xpp.setInput(new InputStreamReader(in)); boolean inWoe = false; boolean inTown = false; boolean inCountry = false; boolean inTimezone = false; int eventType = xpp.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { String tagName = xpp.getName(); if (eventType == XmlPullParser.START_TAG && "woeid".equals(tagName)) { inWoe = true; } else if (eventType == XmlPullParser.TEXT && inWoe) { primaryWoeid = xpp.getText(); } else if (eventType == XmlPullParser.START_TAG && tagName.startsWith("timezone")) { inTimezone = true; } else if (eventType == XmlPullParser.TEXT && inTimezone) { li.timezone = xpp.getText(); } else if (eventType == XmlPullParser.START_TAG && (tagName.startsWith("locality") || tagName.startsWith("admin") || tagName.startsWith("country"))) { for (int i = xpp.getAttributeCount() - 1; i >= 0; i--) { String attrName = xpp.getAttributeName(i); if ("type".equals(attrName) && "Town".equals(xpp.getAttributeValue(i))) { inTown = true; } else if ("type".equals(attrName) && "Country".equals(xpp.getAttributeValue(i))) { inCountry = true; } else if ("woeid".equals(attrName)) { String woeid = xpp.getAttributeValue(i); if (!TextUtils.isEmpty(woeid)) { alternateWoeids.addLast( new Pair<>(tagName, woeid)); } } } } else if (eventType == XmlPullParser.TEXT && inTown) { li.town = xpp.getText(); } else if (eventType == XmlPullParser.TEXT && inCountry) { li.country = xpp.getText(); } if (eventType == XmlPullParser.END_TAG) { inWoe = false; inTown = false; inCountry = false; inTimezone = false; } eventType = xpp.next(); } // Add the primary woeid if it was found. if (!TextUtils.isEmpty(primaryWoeid)) { li.woeids.addLast(primaryWoeid); } // Sort by descending tag name to order by decreasing precision // (locality3, locality2, locality1, admin3, admin2, admin1, etc.) ArrayUtils.sort(alternateWoeids, new Comparator<Pair<String, String>>() { @Override public int compare(Pair<String, String> pair1, Pair<String, String> pair2) { return pair1.first.compareTo(pair2.first); } }); int N = alternateWoeids.size(); for (int i = 0; i < N; i++) { Pair<String, String> pair = alternateWoeids.get(i); li.woeids.addLast(pair.second); } if (li.woeids.size() > 0) { return li; } throw new CantGetWeatherException(true, R.string.no_weather_data, "No WOEIDs found nearby."); } public static CircularArray<LocationSearchResult> findLocationsAutocomplete(String startsWith) { CircularArray<LocationSearchResult> results = new CircularArray<>(); HttpURLConnection connection = null; try { connection = Utils.openUrlConnection(buildPlaceSearchStartsWithUrl(startsWith)); InputStream inputStream = connection.getInputStream(); parseLocationSearchResults(results, inputStream); } catch (IOException | XmlPullParserException e) { Log.w(TAG, "Error parsing place search XML"); } finally { if (connection != null) { connection.disconnect(); } } return results; } private static String buildPlaceSearchStartsWithUrl(String startsWith) { // GeoPlanet API startsWith = startsWith.replaceAll("[^\\w ]+", "").replaceAll(" ", "%20"); return "http://where.yahooapis.com/v1/places.q('" + startsWith + "%2A');" + "count=" + MAX_SEARCH_RESULTS + "?appid=" + YahooWeatherApiConfig.APP_ID; } @VisibleForTesting static void parseLocationSearchResults( CircularArray<LocationSearchResult> results, InputStream inputStream) throws XmlPullParserException, IOException { XmlPullParser xpp = sXmlPullParserFactory.newPullParser(); xpp.setInput(new InputStreamReader(inputStream)); LocationSearchResult result = null; String name = null, country = null, admin1 = null; String timezone = null; StringBuilder sb = new StringBuilder(); int state = PARSE_STATE_NONE; int eventType = xpp.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { String tagName = xpp.getName(); if (eventType == XmlPullParser.START_TAG) { switch (state) { case PARSE_STATE_NONE: if ("place".equals(tagName)) { state = PARSE_STATE_PLACE; result = new LocationSearchResult(); name = country = admin1 = null; } break; case PARSE_STATE_PLACE: if ("name".equals(tagName)) { state = PARSE_STATE_NAME; } else if ("woeid".equals(tagName)) { state = PARSE_STATE_WOEID; } else if ("country".equals(tagName)) { state = PARSE_STATE_COUNTRY; } else if ("admin1".equals(tagName)) { state = PARSE_STATE_ADMIN1; } else if ("timezone".equals(tagName)) { state = PARSE_STATE_TIMEZONE; } break; } } else if (eventType == XmlPullParser.TEXT) { switch (state) { case PARSE_STATE_WOEID: result.woeid = xpp.getText(); break; case PARSE_STATE_NAME: name = xpp.getText(); break; case PARSE_STATE_COUNTRY: country = xpp.getText(); break; case PARSE_STATE_ADMIN1: admin1 = xpp.getText(); break; case PARSE_STATE_TIMEZONE: timezone = xpp.getText(); break; } } else if (eventType == XmlPullParser.END_TAG) { if ("place".equals(tagName)) { // // Sort by descending tag name to order by decreasing precision // // (locality3, locality2, locality1, admin3, admin2, admin1, etc.) // Collections.sort(alternateWoeids, new Comparator<Pair<String, String>>() { // @Override // public int compare(Pair<String, String> pair1, // Pair<String, String> pair2) { // return pair1.first.compareTo(pair2.first); // } // }); sb.setLength(0); if (!TextUtils.isEmpty(name)) { sb.append(name); } if (!TextUtils.isEmpty(admin1)) { if (sb.length() > 0) { sb.append(", "); } sb.append(admin1); } result.displayName = sb.toString(); result.country = country; result.timezone = timezone; results.addLast(result); state = PARSE_STATE_NONE; } else if (state != PARSE_STATE_NONE) { state = PARSE_STATE_PLACE; } } eventType = xpp.next(); } } static class YahooWeatherApiConfig { public static final String APP_ID = "kGO140TV34HVTae_DDS93fM_w3AJmtmI23gxUFnHKWyrOGcRzoFjYpw8Ato6BxhvbTg-"; } public static class LocationInfo { // Sorted by decreasing precision // (point of interest, locality3, locality2, locality1, admin3, admin2, admin1, etc.) public final CircularArray<String> woeids = new CircularArray<>(); public String town; public String country; public String timezone; } public static class LocationSearchResult implements Parcelable { public static final Creator<LocationSearchResult> CREATOR = new Creator<LocationSearchResult>() { @Override public LocationSearchResult createFromParcel(Parcel in) { return new LocationSearchResult(in); } @Override public LocationSearchResult[] newArray(int size) { return new LocationSearchResult[size]; } }; public String woeid; public String displayName; public String country; public String timezone; public LocationSearchResult() { } protected LocationSearchResult(Parcel in) { woeid = in.readString(); displayName = in.readString(); country = in.readString(); timezone = in.readString(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(woeid); dest.writeString(displayName); dest.writeString(country); dest.writeString(timezone); } } }