package com.appsimobile.appsihomeplugins.home;
import android.app.AlarmManager;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import com.appsimobile.appsihomeplugins.DashClockHomeExtension;
import com.appsimobile.appsihomeplugins.R;
import com.appsimobile.appsihomeplugins.dashclock.LogUtils;
import com.appsimobile.appsihomeplugins.dashclock.configuration.AppChooserPreference;
import com.appsimobile.appsihomeplugins.dashclock.weather.CantGetWeatherException;
import com.appsimobile.appsihomeplugins.dashclock.weather.WeatherData;
import com.appsimobile.appsihomeplugins.dashclock.weather.WeatherLocationPreference;
import com.appsimobile.appsihomeplugins.dashclock.weather.WeatherRetryReceiver;
import com.appsimobile.appsihomeplugins.dashclock.weather.YahooWeatherApiClient;
import com.appsimobile.appsisupport.home.HomeServiceContract;
import java.util.Arrays;
import java.util.Locale;
import static com.appsimobile.appsihomeplugins.dashclock.LogUtils.LOGD;
import static com.appsimobile.appsihomeplugins.dashclock.LogUtils.LOGE;
import static com.appsimobile.appsihomeplugins.dashclock.LogUtils.LOGW;
import static com.appsimobile.appsihomeplugins.dashclock.weather.YahooWeatherApiClient.getLocationInfo;
import static com.appsimobile.appsihomeplugins.dashclock.weather.YahooWeatherApiClient.getWeatherForLocationInfo;
/**
* Created by nnma on 9/5/13.
*/
public class WeatherService extends IntentService {
private static final String TAG = LogUtils.makeLogTag(WeatherService.class);
public static final String PREF_WEATHER_UNITS = "pref_weather_units";
public static final String PREF_WEATHER_SHORTCUT = "pref_weather_shortcut";
public static final String PREF_WEATHER_LOCATION = "pref_weather_location";
public static final Intent DEFAULT_WEATHER_INTENT = new Intent(Intent.ACTION_VIEW,
Uri.parse("https://www.google.com/search?q=weather"));
public static final String STATE_UPDATE_IN_PROGRESS = "state_update_in_progress";
public static final String STATE_WEATHER_LAST_BACKOFF_MILLIS
= "state_weather_last_backoff_millis";
public static final String STATE_WEATHER_LAST_UPDATE_ELAPSED_MILLIS
= "state_weather_last_update_elapsed_millis";
private static final long UPDATE_THROTTLE_MILLIS = 10 * DateUtils.MINUTE_IN_MILLIS; // At least 10 min b/w updates
private static final long STALE_LOCATION_MILLIS = 10l * 60000000l; // 10 minutes
private static final int INITIAL_BACKOFF_MILLIS = 30000; // 30 seconds for first error retry
private static final int LOCATION_TIMEOUT_MILLIS = 60000; // 60 sec timeout for location attempt
private static final Criteria sLocationCriteria;
public static Intent sWeatherIntent;
private boolean mOneTimeLocationListenerActive = false;
private Handler mTimeoutHandler = new Handler();
static {
sLocationCriteria = new Criteria();
sLocationCriteria.setPowerRequirement(Criteria.POWER_LOW);
sLocationCriteria.setAccuracy(Criteria.ACCURACY_COARSE);
sLocationCriteria.setCostAllowed(false);
}
public WeatherService() {
super("WeatherService");
}
@Override
public void onCreate() {
super.onCreate();
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.edit().putBoolean(STATE_UPDATE_IN_PROGRESS, false).apply();
}
@Override
protected void onHandleIntent(Intent intent) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
boolean updateInProgress = sp.getBoolean(STATE_UPDATE_IN_PROGRESS, false);
if (updateInProgress) return;
sWeatherIntent = AppChooserPreference.getIntentValue(
sp.getString(PREF_WEATHER_SHORTCUT, null), DEFAULT_WEATHER_INTENT);
long lastUpdateElapsedMillis = sp.getLong(STATE_WEATHER_LAST_UPDATE_ELAPSED_MILLIS, -UPDATE_THROTTLE_MILLIS);
long nowElapsedMillis = SystemClock.elapsedRealtime();
if (nowElapsedMillis > lastUpdateElapsedMillis + UPDATE_THROTTLE_MILLIS) {
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
am.cancel(WeatherRetryReceiver.getPendingIntent(this));
sp.edit().putBoolean(STATE_UPDATE_IN_PROGRESS, true).apply();
updateWeatherData(sp);
} else {
stopSelf();
}
}
private void resetAndCancelRetries() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.edit().remove(STATE_WEATHER_LAST_BACKOFF_MILLIS).remove(STATE_UPDATE_IN_PROGRESS).apply();
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
am.cancel(WeatherRetryReceiver.getPendingIntent(this));
stopSelf();
}
private void scheduleRetry() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
int lastBackoffMillis = sp.getInt(STATE_WEATHER_LAST_BACKOFF_MILLIS, 0);
int backoffMillis = (lastBackoffMillis > 0)
? lastBackoffMillis * 2
: INITIAL_BACKOFF_MILLIS;
sp.edit().putInt(STATE_WEATHER_LAST_BACKOFF_MILLIS, backoffMillis).apply();
LOGD(TAG, "Scheduling weather retry in " + (backoffMillis / 1000) + " second(s)");
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
am.set(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + backoffMillis,
WeatherRetryReceiver.getPendingIntent(this));
}
private void updateWeatherData(SharedPreferences sp) {
NetworkInfo ni = ((ConnectivityManager) getSystemService(
Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
if (ni == null || !ni.isConnected()) {
LOGD(TAG, "No network connection; not attempting to update weather.");
return;
}
String manualLocationWoeid = WeatherLocationPreference.getWoeidFromValue(
sp.getString(PREF_WEATHER_LOCATION, null));
if (!TextUtils.isEmpty(manualLocationWoeid)) {
// WOEIDs
// Honolulu = 2423945
// Paris = 615702
// London = 44418
// New York = 2459115
// San Francisco = 2487956
YahooWeatherApiClient.LocationInfo locationInfo = new YahooWeatherApiClient.LocationInfo();
locationInfo.woeids = Arrays.asList(manualLocationWoeid);
tryPublishWeatherUpdateFromLocationInfo(locationInfo);
return;
}
LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
String provider = lm.getBestProvider(sLocationCriteria, true);
if (TextUtils.isEmpty(provider)) {
publishErrorUpdate(new CantGetWeatherException(false, R.string.no_location_data, "No available location providers matching criteria."));
return;
}
final Location lastLocation = lm.getLastKnownLocation(provider);
if (lastLocation == null ||
(System.currentTimeMillis() - lastLocation.getTime())
>= STALE_LOCATION_MILLIS) {
LOGW(TAG, "Stale or missing last-known location; requesting single coarse location "
+ "update.");
disableOneTimeLocationListener();
mOneTimeLocationListenerActive = true;
lm.requestSingleUpdate(provider, mOneTimeLocationListener, null);
// Time-out single location update request
mTimeoutHandler.removeCallbacksAndMessages(null);
mTimeoutHandler.postDelayed(new Runnable() {
@Override
public void run() {
LOGE(TAG, "Location request timed out.");
disableOneTimeLocationListener();
scheduleRetry();
}
}, LOCATION_TIMEOUT_MILLIS);
} else {
tryPublishWeatherUpdateFromGeolocation(lastLocation);
}
}
private void disableOneTimeLocationListener() {
if (mOneTimeLocationListenerActive) {
LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
lm.removeUpdates(mOneTimeLocationListener);
mOneTimeLocationListenerActive = false;
}
}
private LocationListener mOneTimeLocationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
LOGD(TAG, "Got network location update");
mTimeoutHandler.removeCallbacksAndMessages(null);
tryPublishWeatherUpdateFromGeolocation(location);
disableOneTimeLocationListener();
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
LOGD(TAG, "Network location provider status change: " + status);
if (status == LocationProvider.TEMPORARILY_UNAVAILABLE) {
scheduleRetry();
disableOneTimeLocationListener();
}
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
};
@Override
public void onDestroy() {
super.onDestroy();
disableOneTimeLocationListener();
}
private void tryPublishWeatherUpdateFromGeolocation(Location location) {
try {
LOGD(TAG, "Using location: " + location.getLatitude() + "," + location.getLongitude());
tryPublishWeatherUpdateFromLocationInfo(getLocationInfo(location));
} catch (CantGetWeatherException e) {
publishErrorUpdate(e);
if (e.isRetryable()) {
scheduleRetry();
}
}
}
private void tryPublishWeatherUpdateFromLocationInfo(YahooWeatherApiClient.LocationInfo locationInfo) {
try {
publishWeatherUpdate(getWeatherForLocationInfo(locationInfo));
} catch (CantGetWeatherException e) {
publishErrorUpdate(e);
if (e.isRetryable()) {
scheduleRetry();
}
}
}
private void publishErrorUpdate(CantGetWeatherException e) {
LOGE(TAG, "Showing a weather extension error", e);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putInt("last_conditionIconId", R.drawable.ic_weather_clear);
editor.putString("last_title", getString(R.string.status_none));
editor.putString("last_text", getString(e.getUserFacingErrorStringId()));
editor.putString("last_intent", sWeatherIntent.toUri(0));
editor.commit();
HomeServiceContract.requestPluginUpdate(this, DashClockHomeExtension.DASHCLOCK_EXTENSION_WEATHER);
}
private void publishWeatherUpdate(WeatherData weatherData) {
String temperature = (weatherData.temperature != WeatherData.INVALID_TEMPERATURE)
? getString(R.string.temperature_template, weatherData.temperature)
: getString(R.string.status_none);
StringBuilder expandedBody = new StringBuilder();
if (weatherData.low != WeatherData.INVALID_TEMPERATURE
&& weatherData.high != WeatherData.INVALID_TEMPERATURE) {
expandedBody.append(getString(R.string.weather_low_high_template,
getString(R.string.temperature_template, weatherData.low),
getString(R.string.temperature_template, weatherData.high)));
}
int conditionIconId = WeatherData.getConditionIconId(weatherData.conditionCode);
if (WeatherData.getConditionIconId(weatherData.todayForecastConditionCode)
== R.drawable.ic_weather_raining) {
// Show rain if it will rain today.
conditionIconId = R.drawable.ic_weather_raining;
if (expandedBody.length() > 0) {
expandedBody.append(", ");
}
expandedBody.append(
getString(R.string.later_forecast_template, weatherData.forecastText));
}
if (expandedBody.length() > 0) {
expandedBody.append("\n");
}
expandedBody.append(weatherData.location);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = sharedPreferences.edit();
String sWeatherUnits = sharedPreferences.getString(WeatherService.PREF_WEATHER_UNITS, "f");
editor.putInt("last_conditionIconId", conditionIconId);
editor.putString("last_title", getString(R.string.weather_expanded_title_template,
temperature + sWeatherUnits.toUpperCase(Locale.US),
weatherData.conditionText));
editor.putString("last_text", expandedBody.toString());
editor.putString("last_intent", sWeatherIntent.toUri(0));
editor.putLong(STATE_WEATHER_LAST_UPDATE_ELAPSED_MILLIS,
SystemClock.elapsedRealtime());
editor.commit();
HomeServiceContract.requestPluginUpdate(this, DashClockHomeExtension.DASHCLOCK_EXTENSION_WEATHER);
// Mark that a successful weather update has been pushed
resetAndCancelRetries();
}
}