diff options
Diffstat (limited to 'src/org/lineageos/lockclock/weather/WeatherUpdateService.java')
-rw-r--r-- | src/org/lineageos/lockclock/weather/WeatherUpdateService.java | 580 |
1 files changed, 580 insertions, 0 deletions
diff --git a/src/org/lineageos/lockclock/weather/WeatherUpdateService.java b/src/org/lineageos/lockclock/weather/WeatherUpdateService.java new file mode 100644 index 0000000..61b7aa0 --- /dev/null +++ b/src/org/lineageos/lockclock/weather/WeatherUpdateService.java @@ -0,0 +1,580 @@ +/* + * Copyright (C) 2012 The CyanogenMod Project + * + * 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.cyanogenmod.lockclock.weather; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.location.Criteria; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; +import com.cyanogenmod.lockclock.ClockWidgetProvider; +import com.cyanogenmod.lockclock.R; +import com.cyanogenmod.lockclock.misc.Constants; +import com.cyanogenmod.lockclock.misc.Preferences; +import com.cyanogenmod.lockclock.misc.WidgetUtils; +import com.cyanogenmod.lockclock.preference.WeatherPreferences; + +import cyanogenmod.weather.CMWeatherManager; +import cyanogenmod.weather.WeatherInfo; +import cyanogenmod.weather.WeatherLocation; + +import org.cyanogenmod.internal.util.PackageManagerUtils; + +import java.lang.ref.WeakReference; +import java.util.Date; + +public class WeatherUpdateService extends Service { + private static final String TAG = "WeatherUpdateService"; + private static final boolean D = Constants.DEBUG; + + public static final String ACTION_FORCE_UPDATE = "com.cyanogenmod.lockclock.action.FORCE_WEATHER_UPDATE"; + private static final String ACTION_CANCEL_LOCATION_UPDATE = + "com.cyanogenmod.lockclock.action.CANCEL_LOCATION_UPDATE"; + + private static final String ACTION_CANCEL_UPDATE_WEATHER_REQUEST = + "com.cyanogenmod.lockclock.action.CANCEL_UPDATE_WEATHER_REQUEST"; + private static final long WEATHER_UPDATE_REQUEST_TIMEOUT_MS = 30L * 1000L; + + // Broadcast action for end of update + public static final String ACTION_UPDATE_FINISHED = "com.cyanogenmod.lockclock.action.WEATHER_UPDATE_FINISHED"; + public static final String EXTRA_UPDATE_CANCELLED = "update_cancelled"; + + private static final long LOCATION_REQUEST_TIMEOUT = 5L * 60L * 1000L; // request for at most 5 minutes + private static final long OUTDATED_LOCATION_THRESHOLD_MILLIS = 10L * 60L * 1000L; // 10 minutes + private static final float LOCATION_ACCURACY_THRESHOLD_METERS = 50000; + + private WorkerThread mWorkerThread; + private Handler mHandler; + + private static final Criteria sLocationCriteria; + static { + sLocationCriteria = new Criteria(); + sLocationCriteria.setPowerRequirement(Criteria.POWER_LOW); + sLocationCriteria.setAccuracy(Criteria.ACCURACY_COARSE); + sLocationCriteria.setCostAllowed(false); + } + + @Override + public void onCreate() { + Log.d(TAG, "onCreate"); + mWorkerThread = new WorkerThread(getApplicationContext()); + mWorkerThread.start(); + mWorkerThread.prepareHandler(); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (D) Log.v(TAG, "Got intent " + intent); + + if (ACTION_CANCEL_LOCATION_UPDATE.equals(intent.getAction())) { + WeatherLocationListener.cancel(this); + if (!mWorkerThread.isProcessing()) { + stopSelf(); + } + return START_NOT_STICKY; + } + + if (ACTION_CANCEL_UPDATE_WEATHER_REQUEST.equals(intent.getAction())) { + if (mWorkerThread.isProcessing()) { + mWorkerThread.getHandler().obtainMessage( + WorkerThread.MSG_CANCEL_UPDATE_WEATHER_REQUEST).sendToTarget(); + mHandler.post(new Runnable() { + @Override + public void run() { + final Context context = getApplicationContext(); + final CMWeatherManager weatherManager + = CMWeatherManager.getInstance(context); + final String activeProviderLabel + = weatherManager.getActiveWeatherServiceProviderLabel(); + final String noData + = getString(R.string.weather_cannot_reach_provider, + activeProviderLabel); + Toast.makeText(context, noData, Toast.LENGTH_SHORT).show(); + } + }); + } + stopSelf(); + return START_NOT_STICKY; + } + + boolean force = ACTION_FORCE_UPDATE.equals(intent.getAction()); + if (!shouldUpdate(force)) { + Log.d(TAG, "Service started, but shouldn't update ... stopping"); + sendCancelledBroadcast(); + stopSelf(); + return START_NOT_STICKY; + } + + mWorkerThread.getHandler().obtainMessage(WorkerThread.MSG_ON_NEW_WEATHER_REQUEST) + .sendToTarget(); + + return START_REDELIVER_INTENT; + } + + private boolean shouldUpdate(boolean force) { + final CMWeatherManager weatherManager + = CMWeatherManager.getInstance(getApplicationContext()); + if (weatherManager.getActiveWeatherServiceProviderLabel() == null) { + //Why bother if we don't even have an active provider + if (D) Log.d(TAG, "No active weather service provider found, skip"); + return false; + } + + final long interval = Preferences.weatherRefreshIntervalInMs(this); + if (interval == 0 && !force) { + if (D) Log.v(TAG, "Interval set to manual and update not forced, skip"); + return false; + } + + if (!WeatherPreferences.hasLocationPermission(this)) { + if (D) Log.v(TAG, "Application does not have the location permission, skip"); + return false; + } + + if (WidgetUtils.isNetworkAvailable(this)) { + if (force) { + if (D) Log.d(TAG, "Forcing weather update"); + return true; + } else { + final long now = SystemClock.elapsedRealtime(); + final long lastUpdate = Preferences.lastWeatherUpdateTimestamp(this); + final long due = lastUpdate + interval; + if (D) { + Log.d(TAG, "Now " + now + " Last update " + lastUpdate + + " interval " + interval); + } + + if (lastUpdate == 0 || due - now < 0) { + if (D) Log.d(TAG, "Should update"); + return true; + } else { + if (D) Log.v(TAG, "Next weather update due in " + (due - now) + " ms, skip"); + return false; + } + } + } else { + if (D) Log.d(TAG, "Network is not available, skip"); + return false; + } + } + + private static class WorkerThread extends HandlerThread + implements CMWeatherManager.WeatherUpdateRequestListener { + + public static final int MSG_ON_NEW_WEATHER_REQUEST = 1; + public static final int MSG_ON_WEATHER_REQUEST_COMPLETED = 2; + public static final int MSG_WEATHER_REQUEST_FAILED = 3; + public static final int MSG_CANCEL_UPDATE_WEATHER_REQUEST = 4; + + private Handler mHandler; + private boolean mIsProcessingWeatherUpdate = false; + private WakeLock mWakeLock; + private PendingIntent mTimeoutPendingIntent; + private int mRequestId; + private final CMWeatherManager mWeatherManager; + final private Context mContext; + + public WorkerThread(Context context) { + super("weather-service-worker"); + mContext = context; + mWeatherManager = CMWeatherManager.getInstance(mContext); + } + + public synchronized void prepareHandler() { + mHandler = new Handler(getLooper()) { + @Override + public void handleMessage(Message msg) { + if (D) Log.d(TAG, "Msg " + msg.what); + switch (msg.what) { + case MSG_ON_NEW_WEATHER_REQUEST: + onNewWeatherRequest(); + break; + case MSG_ON_WEATHER_REQUEST_COMPLETED: + WeatherInfo info = (WeatherInfo) msg.obj; + onWeatherRequestCompleted(info); + break; + case MSG_WEATHER_REQUEST_FAILED: + int status = msg.arg1; + onWeatherRequestFailed(status); + break; + case MSG_CANCEL_UPDATE_WEATHER_REQUEST: + onCancelUpdateWeatherRequest(); + break; + default: + //Unknown message, pass it on... + super.handleMessage(msg); + } + } + }; + } + + private void startTimeoutAlarm() { + Intent intent = new Intent(mContext, WeatherUpdateService.class); + intent.setAction(ACTION_CANCEL_UPDATE_WEATHER_REQUEST); + + mTimeoutPendingIntent = PendingIntent.getService(mContext, 0, intent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT); + + AlarmManager am = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); + long elapseTime = SystemClock.elapsedRealtime() + WEATHER_UPDATE_REQUEST_TIMEOUT_MS; + am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, elapseTime, mTimeoutPendingIntent); + if (D) Log.v(TAG, "Timeout alarm set to expire in " + elapseTime + " ms"); + } + + private void cancelTimeoutAlarm() { + if (mTimeoutPendingIntent != null) { + AlarmManager am = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); + am.cancel(mTimeoutPendingIntent); + mTimeoutPendingIntent = null; + if (D) Log.v(TAG, "Timeout alarm cancelled"); + } + } + + public synchronized Handler getHandler() { + return mHandler; + } + + private void onNewWeatherRequest() { + if (mIsProcessingWeatherUpdate) { + Log.d(TAG, "Already processing weather update, discarding request..."); + return; + } + + mIsProcessingWeatherUpdate = true; + final PowerManager pm + = (PowerManager) mContext.getSystemService(POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setReferenceCounted(false); + if (D) Log.v(TAG, "ACQUIRING WAKELOCK"); + mWakeLock.acquire(); + + WeatherLocation customWeatherLocation = null; + if (Preferences.useCustomWeatherLocation(mContext)) { + customWeatherLocation = Preferences.getCustomWeatherLocation(mContext); + } + if (customWeatherLocation != null) { + mRequestId = mWeatherManager.requestWeatherUpdate(customWeatherLocation, this); + if (D) Log.d(TAG, "Request submitted using WeatherLocation"); + startTimeoutAlarm(); + } else { + final Location location = getCurrentLocation(); + if (location != null) { + mRequestId = mWeatherManager.requestWeatherUpdate(location, this); + if (D) Log.d(TAG, "Request submitted using Location"); + startTimeoutAlarm(); + } else { + // work with cached location from last request for now + // a listener to update it is already scheduled if possible + WeatherInfo cachedInfo = Preferences.getCachedWeatherInfo(mContext); + if (cachedInfo != null) { + mHandler.obtainMessage(MSG_ON_WEATHER_REQUEST_COMPLETED, + cachedInfo).sendToTarget(); + if (D) Log.d(TAG, "Returning cached weather data [ " + + cachedInfo.toString()+ " ]"); + } else { + mHandler.obtainMessage(MSG_WEATHER_REQUEST_FAILED, + CMWeatherManager.RequestStatus.FAILED, 0).sendToTarget(); + } + } + } + } + + public void tearDown() { + if (D) Log.d(TAG, "Tearing down worker thread"); + if (isProcessing()) mWeatherManager.cancelRequest(mRequestId); + quit(); + } + + public boolean isProcessing() { + return mIsProcessingWeatherUpdate; + } + + private Location getCurrentLocation() { + final LocationManager lm + = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); + Location location = lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER); + if (D) Log.v(TAG, "Current location is " + location); + + if (location != null && location.getAccuracy() > LOCATION_ACCURACY_THRESHOLD_METERS) { + if (D) Log.d(TAG, "Ignoring inaccurate location"); + location = null; + } + + // If lastKnownLocation is not present (because none of the apps in the + // device has requested the current location to the system yet) or outdated, + // then try to get the current location use the provider that best matches the criteria. + boolean needsUpdate = location == null; + if (location != null) { + long delta = System.currentTimeMillis() - location.getTime(); + needsUpdate = delta > OUTDATED_LOCATION_THRESHOLD_MILLIS; + } + if (needsUpdate) { + if (D) Log.d(TAG, "Getting best location provider"); + String locationProvider = lm.getBestProvider(sLocationCriteria, true); + if (TextUtils.isEmpty(locationProvider)) { + Log.e(TAG, "No available location providers matching criteria."); + } else if (PackageManagerUtils.isAppInstalled(mContext, "com.google.android.gms") + && locationProvider.equals(LocationManager.GPS_PROVIDER)) { + // Since Google Play services is available, + // let's conserve battery power and not depend on the device's GPS. + Log.i(TAG, "Google Play Services available; Ignoring GPS provider."); + } else { + WeatherLocationListener.registerIfNeeded(mContext, locationProvider); + } + } + return location; + } + + private void onWeatherRequestCompleted(WeatherInfo result) { + if (D) Log.d(TAG, "Weather update received, caching data and updating widget"); + cancelTimeoutAlarm(); + long now = SystemClock.elapsedRealtime(); + Preferences.setCachedWeatherInfo(mContext, now, result); + Preferences.setLastWeatherUpdateTimestamp(mContext, now); + scheduleUpdate(mContext, Preferences.weatherRefreshIntervalInMs(mContext), false); + + Intent updateIntent = new Intent(mContext, ClockWidgetProvider.class); + mContext.sendBroadcast(updateIntent); + broadcastAndCleanUp(false); + } + + private void onWeatherRequestFailed(int status) { + if (D) Log.d(TAG, "Weather refresh failed ["+status+"]"); + cancelTimeoutAlarm(); + if (status == CMWeatherManager.RequestStatus.ALREADY_IN_PROGRESS) { + if (D) Log.d(TAG, "A request is already in progress, no need to schedule again"); + } else if (status == CMWeatherManager.RequestStatus.FAILED) { + //Something went wrong, let's schedule an update at the next interval from now + //A force update might happen earlier anyway + scheduleUpdate(mContext, Preferences.weatherRefreshIntervalInMs(mContext), false); + } else { + //Wait until the next update is due + scheduleNextUpdate(mContext, false); + } + broadcastAndCleanUp(true); + } + + private void onCancelUpdateWeatherRequest() { + if (D) Log.d(TAG, "Cancelling active weather request"); + if (mIsProcessingWeatherUpdate) { + cancelTimeoutAlarm(); + mWeatherManager.cancelRequest(mRequestId); + broadcastAndCleanUp(true); + } + } + + private void broadcastAndCleanUp(boolean updateCancelled) { + Intent finishedIntent = new Intent(ACTION_UPDATE_FINISHED); + finishedIntent.putExtra(EXTRA_UPDATE_CANCELLED, updateCancelled); + mContext.sendBroadcast(finishedIntent); + + if (D) Log.d(TAG, "RELEASING WAKELOCK"); + mWakeLock.release(); + mIsProcessingWeatherUpdate = false; + mContext.stopService(new Intent(mContext, WeatherUpdateService.class)); + } + + @Override + public void onWeatherRequestCompleted(int state, WeatherInfo weatherInfo) { + if (state == CMWeatherManager.RequestStatus.COMPLETED) { + mHandler.obtainMessage(WorkerThread.MSG_ON_WEATHER_REQUEST_COMPLETED, weatherInfo) + .sendToTarget(); + } else { + mHandler.obtainMessage(WorkerThread.MSG_WEATHER_REQUEST_FAILED, state, 0) + .sendToTarget(); + } + } + } + + private void sendCancelledBroadcast() { + Intent finishedIntent = new Intent(ACTION_UPDATE_FINISHED); + finishedIntent.putExtra(EXTRA_UPDATE_CANCELLED, true); + sendBroadcast(finishedIntent); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy"); + mWorkerThread.tearDown(); + } + + private static class WeatherLocationListener implements LocationListener { + private Context mContext; + private PendingIntent mTimeoutIntent; + private static WeatherLocationListener sInstance = null; + + static void registerIfNeeded(Context context, String provider) { + synchronized (WeatherLocationListener.class) { + if (D) Log.d(TAG, "Registering location listener"); + if (sInstance == null) { + final Context appContext = context.getApplicationContext(); + final LocationManager locationManager = + (LocationManager) appContext.getSystemService(Context.LOCATION_SERVICE); + + // Check location provider after set sInstance, so, if the provider is not + // supported, we never enter here again. + sInstance = new WeatherLocationListener(appContext); + // Check whether the provider is supported. + // NOTE!!! Actually only WeatherUpdateService class is calling this function + // with the NETWORK_PROVIDER, so setting the instance is safe. We must + // change this if this call receive different providers + LocationProvider lp = locationManager.getProvider(provider); + if (lp != null) { + if (D) Log.d(TAG, "LocationManager - Requesting single update"); + locationManager.requestSingleUpdate(provider, sInstance, + appContext.getMainLooper()); + sInstance.setTimeoutAlarm(); + } + } + } + } + + static void cancel(Context context) { + synchronized (WeatherLocationListener.class) { + if (sInstance != null) { + final Context appContext = context.getApplicationContext(); + final LocationManager locationManager = + (LocationManager) appContext.getSystemService(Context.LOCATION_SERVICE); + if (D) Log.d(TAG, "Aborting location request after timeout"); + locationManager.removeUpdates(sInstance); + sInstance.cancelTimeoutAlarm(); + sInstance = null; + } + } + } + + private WeatherLocationListener(Context context) { + super(); + mContext = context; + } + + private void setTimeoutAlarm() { + Intent intent = new Intent(mContext, WeatherUpdateService.class); + intent.setAction(ACTION_CANCEL_LOCATION_UPDATE); + + mTimeoutIntent = PendingIntent.getService(mContext, 0, intent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT); + + AlarmManager am = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); + long elapseTime = SystemClock.elapsedRealtime() + LOCATION_REQUEST_TIMEOUT; + am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, elapseTime, mTimeoutIntent); + } + + private void cancelTimeoutAlarm() { + if (mTimeoutIntent != null) { + AlarmManager am = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); + am.cancel(mTimeoutIntent); + mTimeoutIntent = null; + } + } + + @Override + public void onLocationChanged(Location location) { + // Now, we have a location to use. Schedule a weather update right now. + if (D) Log.d(TAG, "The location has changed, schedule an update "); + synchronized (WeatherLocationListener.class) { + scheduleUpdate(mContext, 0, true); + cancelTimeoutAlarm(); + sInstance = null; + } + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + // Now, we have a location to use. Schedule a weather update right now. + if (D) Log.d(TAG, "The location service has become available, schedule an update "); + if (status == LocationProvider.AVAILABLE) { + synchronized (WeatherLocationListener.class) { + scheduleUpdate(mContext, 0, true); + cancelTimeoutAlarm(); + sInstance = null; + } + } + } + + @Override + public void onProviderEnabled(String provider) { + // Not used + } + + @Override + public void onProviderDisabled(String provider) { + // Not used + } + } + + private static void scheduleUpdate(Context context, long millisFromNow, boolean force) { + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + long due = SystemClock.elapsedRealtime() + millisFromNow; + if (D) Log.d(TAG, "Next update scheduled at " + + new Date(System.currentTimeMillis() + millisFromNow)); + am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, due, getUpdateIntent(context, force)); + } + + public static void scheduleNextUpdate(Context context, boolean force) { + if (force) { + if (D) Log.d(TAG, "Scheduling next update immediately"); + scheduleUpdate(context, 0, true); + } else { + final long lastUpdate = Preferences.lastWeatherUpdateTimestamp(context); + final long interval = Preferences.weatherRefreshIntervalInMs(context); + final long now = SystemClock.elapsedRealtime(); + long due = (interval + lastUpdate) - now; + if (due < 0) due = 0; + if (D) Log.d(TAG, "Scheduling in " + due + " ms"); + scheduleUpdate(context, due, false); + } + } + + public static PendingIntent getUpdateIntent(Context context, boolean force) { + Intent i = new Intent(context, WeatherUpdateService.class); + if (force) { + i.setAction(ACTION_FORCE_UPDATE); + } + return PendingIntent.getService(context, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); + } + + public static void cancelUpdates(Context context) { + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + am.cancel(getUpdateIntent(context, true)); + am.cancel(getUpdateIntent(context, false)); + WeatherLocationListener.cancel(context); + } +} |