TwilightService.java revision 9158825f9c41869689d6b1786d7c7aa8bdd524ce
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.server.twilight; 18 19import com.android.server.SystemService; 20import com.android.server.TwilightCalculator; 21 22import android.app.AlarmManager; 23import android.app.PendingIntent; 24import android.content.BroadcastReceiver; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.location.Criteria; 29import android.location.Location; 30import android.location.LocationListener; 31import android.location.LocationManager; 32import android.os.Bundle; 33import android.os.Handler; 34import android.os.Message; 35import android.os.SystemClock; 36import android.text.format.DateUtils; 37import android.text.format.Time; 38import android.util.Slog; 39 40import java.util.ArrayList; 41import java.util.Iterator; 42 43import libcore.util.Objects; 44 45/** 46 * Figures out whether it's twilight time based on the user's location. 47 * 48 * Used by the UI mode manager and other components to adjust night mode 49 * effects based on sunrise and sunset. 50 */ 51public final class TwilightService extends SystemService { 52 static final String TAG = "TwilightService"; 53 static final boolean DEBUG = false; 54 static final String ACTION_UPDATE_TWILIGHT_STATE = 55 "com.android.server.action.UPDATE_TWILIGHT_STATE"; 56 57 final Object mLock = new Object(); 58 59 AlarmManager mAlarmManager; 60 LocationManager mLocationManager; 61 LocationHandler mLocationHandler; 62 63 final ArrayList<TwilightListenerRecord> mListeners = 64 new ArrayList<TwilightListenerRecord>(); 65 66 TwilightState mTwilightState; 67 68 @Override 69 public void onStart() { 70 mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); 71 mLocationManager = (LocationManager) getContext().getSystemService( 72 Context.LOCATION_SERVICE); 73 mLocationHandler = new LocationHandler(); 74 75 IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); 76 filter.addAction(Intent.ACTION_TIME_CHANGED); 77 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 78 filter.addAction(ACTION_UPDATE_TWILIGHT_STATE); 79 getContext().registerReceiver(mUpdateLocationReceiver, filter); 80 81 publishLocalService(TwilightManager.class, mService); 82 } 83 84 private static class TwilightListenerRecord implements Runnable { 85 private final TwilightListener mListener; 86 private final Handler mHandler; 87 88 public TwilightListenerRecord(TwilightListener listener, Handler handler) { 89 mListener = listener; 90 mHandler = handler; 91 } 92 93 public void postUpdate() { 94 mHandler.post(this); 95 } 96 97 @Override 98 public void run() { 99 mListener.onTwilightStateChanged(); 100 } 101 102 } 103 104 private final TwilightManager mService = new TwilightManager() { 105 /** 106 * Gets the current twilight state. 107 * 108 * @return The current twilight state, or null if no information is available. 109 */ 110 @Override 111 public TwilightState getCurrentState() { 112 synchronized (mLock) { 113 return mTwilightState; 114 } 115 } 116 117 /** 118 * Listens for twilight time. 119 * 120 * @param listener The listener. 121 */ 122 @Override 123 public void registerListener(TwilightListener listener, Handler handler) { 124 synchronized (mLock) { 125 mListeners.add(new TwilightListenerRecord(listener, handler)); 126 127 if (mListeners.size() == 1) { 128 mLocationHandler.enableLocationUpdates(); 129 } 130 } 131 } 132 }; 133 134 private void setTwilightState(TwilightState state) { 135 synchronized (mLock) { 136 if (!Objects.equal(mTwilightState, state)) { 137 if (DEBUG) { 138 Slog.d(TAG, "Twilight state changed: " + state); 139 } 140 141 mTwilightState = state; 142 143 final int listenerLen = mListeners.size(); 144 for (int i = 0; i < listenerLen; i++) { 145 mListeners.get(i).postUpdate(); 146 } 147 } 148 } 149 } 150 151 // The user has moved if the accuracy circles of the two locations don't overlap. 152 private static boolean hasMoved(Location from, Location to) { 153 if (to == null) { 154 return false; 155 } 156 157 if (from == null) { 158 return true; 159 } 160 161 // if new location is older than the current one, the device hasn't moved. 162 if (to.getElapsedRealtimeNanos() < from.getElapsedRealtimeNanos()) { 163 return false; 164 } 165 166 // Get the distance between the two points. 167 float distance = from.distanceTo(to); 168 169 // Get the total accuracy radius for both locations. 170 float totalAccuracy = from.getAccuracy() + to.getAccuracy(); 171 172 // If the distance is greater than the combined accuracy of the two 173 // points then they can't overlap and hence the user has moved. 174 return distance >= totalAccuracy; 175 } 176 177 private final class LocationHandler extends Handler { 178 private static final int MSG_ENABLE_LOCATION_UPDATES = 1; 179 private static final int MSG_GET_NEW_LOCATION_UPDATE = 2; 180 private static final int MSG_PROCESS_NEW_LOCATION = 3; 181 private static final int MSG_DO_TWILIGHT_UPDATE = 4; 182 183 private static final long LOCATION_UPDATE_MS = 24 * DateUtils.HOUR_IN_MILLIS; 184 private static final long MIN_LOCATION_UPDATE_MS = 30 * DateUtils.MINUTE_IN_MILLIS; 185 private static final float LOCATION_UPDATE_DISTANCE_METER = 1000 * 20; 186 private static final long LOCATION_UPDATE_ENABLE_INTERVAL_MIN = 5000; 187 private static final long LOCATION_UPDATE_ENABLE_INTERVAL_MAX = 188 15 * DateUtils.MINUTE_IN_MILLIS; 189 private static final double FACTOR_GMT_OFFSET_LONGITUDE = 190 1000.0 * 360.0 / DateUtils.DAY_IN_MILLIS; 191 192 private boolean mPassiveListenerEnabled; 193 private boolean mNetworkListenerEnabled; 194 private boolean mDidFirstInit; 195 private long mLastNetworkRegisterTime = -MIN_LOCATION_UPDATE_MS; 196 private long mLastUpdateInterval; 197 private Location mLocation; 198 private final TwilightCalculator mTwilightCalculator = new TwilightCalculator(); 199 200 public void processNewLocation(Location location) { 201 Message msg = obtainMessage(MSG_PROCESS_NEW_LOCATION, location); 202 sendMessage(msg); 203 } 204 205 public void enableLocationUpdates() { 206 sendEmptyMessage(MSG_ENABLE_LOCATION_UPDATES); 207 } 208 209 public void requestLocationUpdate() { 210 sendEmptyMessage(MSG_GET_NEW_LOCATION_UPDATE); 211 } 212 213 public void requestTwilightUpdate() { 214 sendEmptyMessage(MSG_DO_TWILIGHT_UPDATE); 215 } 216 217 @Override 218 public void handleMessage(Message msg) { 219 switch (msg.what) { 220 case MSG_PROCESS_NEW_LOCATION: { 221 final Location location = (Location)msg.obj; 222 final boolean hasMoved = hasMoved(mLocation, location); 223 final boolean hasBetterAccuracy = mLocation == null 224 || location.getAccuracy() < mLocation.getAccuracy(); 225 if (DEBUG) { 226 Slog.d(TAG, "Processing new location: " + location 227 + ", hasMoved=" + hasMoved 228 + ", hasBetterAccuracy=" + hasBetterAccuracy); 229 } 230 if (hasMoved || hasBetterAccuracy) { 231 setLocation(location); 232 } 233 break; 234 } 235 236 case MSG_GET_NEW_LOCATION_UPDATE: 237 if (!mNetworkListenerEnabled) { 238 // Don't do anything -- we are still trying to get a 239 // location. 240 return; 241 } 242 if ((mLastNetworkRegisterTime + MIN_LOCATION_UPDATE_MS) >= 243 SystemClock.elapsedRealtime()) { 244 // Don't do anything -- it hasn't been long enough 245 // since we last requested an update. 246 return; 247 } 248 249 // Unregister the current location monitor, so we can 250 // register a new one for it to get an immediate update. 251 mNetworkListenerEnabled = false; 252 mLocationManager.removeUpdates(mEmptyLocationListener); 253 254 // Fall through to re-register listener. 255 case MSG_ENABLE_LOCATION_UPDATES: 256 // enable network provider to receive at least location updates for a given 257 // distance. 258 boolean networkLocationEnabled; 259 try { 260 networkLocationEnabled = 261 mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER); 262 } catch (Exception e) { 263 // we may get IllegalArgumentException if network location provider 264 // does not exist or is not yet installed. 265 networkLocationEnabled = false; 266 } 267 if (!mNetworkListenerEnabled && networkLocationEnabled) { 268 mNetworkListenerEnabled = true; 269 mLastNetworkRegisterTime = SystemClock.elapsedRealtime(); 270 mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 271 LOCATION_UPDATE_MS, 0, mEmptyLocationListener); 272 273 if (!mDidFirstInit) { 274 mDidFirstInit = true; 275 if (mLocation == null) { 276 retrieveLocation(); 277 } 278 } 279 } 280 281 // enable passive provider to receive updates from location fixes (gps 282 // and network). 283 boolean passiveLocationEnabled; 284 try { 285 passiveLocationEnabled = 286 mLocationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER); 287 } catch (Exception e) { 288 // we may get IllegalArgumentException if passive location provider 289 // does not exist or is not yet installed. 290 passiveLocationEnabled = false; 291 } 292 293 if (!mPassiveListenerEnabled && passiveLocationEnabled) { 294 mPassiveListenerEnabled = true; 295 mLocationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 296 0, LOCATION_UPDATE_DISTANCE_METER , mLocationListener); 297 } 298 299 if (!(mNetworkListenerEnabled && mPassiveListenerEnabled)) { 300 mLastUpdateInterval *= 1.5; 301 if (mLastUpdateInterval == 0) { 302 mLastUpdateInterval = LOCATION_UPDATE_ENABLE_INTERVAL_MIN; 303 } else if (mLastUpdateInterval > LOCATION_UPDATE_ENABLE_INTERVAL_MAX) { 304 mLastUpdateInterval = LOCATION_UPDATE_ENABLE_INTERVAL_MAX; 305 } 306 sendEmptyMessageDelayed(MSG_ENABLE_LOCATION_UPDATES, mLastUpdateInterval); 307 } 308 break; 309 310 case MSG_DO_TWILIGHT_UPDATE: 311 updateTwilightState(); 312 break; 313 } 314 } 315 316 private void retrieveLocation() { 317 Location location = null; 318 final Iterator<String> providers = 319 mLocationManager.getProviders(new Criteria(), true).iterator(); 320 while (providers.hasNext()) { 321 final Location lastKnownLocation = 322 mLocationManager.getLastKnownLocation(providers.next()); 323 // pick the most recent location 324 if (location == null || (lastKnownLocation != null && 325 location.getElapsedRealtimeNanos() < 326 lastKnownLocation.getElapsedRealtimeNanos())) { 327 location = lastKnownLocation; 328 } 329 } 330 331 // In the case there is no location available (e.g. GPS fix or network location 332 // is not available yet), the longitude of the location is estimated using the timezone, 333 // latitude and accuracy are set to get a good average. 334 if (location == null) { 335 Time currentTime = new Time(); 336 currentTime.set(System.currentTimeMillis()); 337 double lngOffset = FACTOR_GMT_OFFSET_LONGITUDE * 338 (currentTime.gmtoff - (currentTime.isDst > 0 ? 3600 : 0)); 339 location = new Location("fake"); 340 location.setLongitude(lngOffset); 341 location.setLatitude(0); 342 location.setAccuracy(417000.0f); 343 location.setTime(System.currentTimeMillis()); 344 location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos()); 345 346 if (DEBUG) { 347 Slog.d(TAG, "Estimated location from timezone: " + location); 348 } 349 } 350 351 setLocation(location); 352 } 353 354 private void setLocation(Location location) { 355 mLocation = location; 356 updateTwilightState(); 357 } 358 359 private void updateTwilightState() { 360 if (mLocation == null) { 361 setTwilightState(null); 362 return; 363 } 364 365 final long now = System.currentTimeMillis(); 366 367 // calculate yesterday's twilight 368 mTwilightCalculator.calculateTwilight(now - DateUtils.DAY_IN_MILLIS, 369 mLocation.getLatitude(), mLocation.getLongitude()); 370 final long yesterdaySunset = mTwilightCalculator.mSunset; 371 372 // calculate today's twilight 373 mTwilightCalculator.calculateTwilight(now, 374 mLocation.getLatitude(), mLocation.getLongitude()); 375 final boolean isNight = (mTwilightCalculator.mState == TwilightCalculator.NIGHT); 376 final long todaySunrise = mTwilightCalculator.mSunrise; 377 final long todaySunset = mTwilightCalculator.mSunset; 378 379 // calculate tomorrow's twilight 380 mTwilightCalculator.calculateTwilight(now + DateUtils.DAY_IN_MILLIS, 381 mLocation.getLatitude(), mLocation.getLongitude()); 382 final long tomorrowSunrise = mTwilightCalculator.mSunrise; 383 384 // set twilight state 385 TwilightState state = new TwilightState(isNight, yesterdaySunset, 386 todaySunrise, todaySunset, tomorrowSunrise); 387 if (DEBUG) { 388 Slog.d(TAG, "Updating twilight state: " + state); 389 } 390 setTwilightState(state); 391 392 // schedule next update 393 long nextUpdate = 0; 394 if (todaySunrise == -1 || todaySunset == -1) { 395 // In the case the day or night never ends the update is scheduled 12 hours later. 396 nextUpdate = now + 12 * DateUtils.HOUR_IN_MILLIS; 397 } else { 398 // add some extra time to be on the safe side. 399 nextUpdate += DateUtils.MINUTE_IN_MILLIS; 400 401 if (now > todaySunset) { 402 nextUpdate += tomorrowSunrise; 403 } else if (now > todaySunrise) { 404 nextUpdate += todaySunset; 405 } else { 406 nextUpdate += todaySunrise; 407 } 408 } 409 410 if (DEBUG) { 411 Slog.d(TAG, "Next update in " + (nextUpdate - now) + " ms"); 412 } 413 414 Intent updateIntent = new Intent(ACTION_UPDATE_TWILIGHT_STATE); 415 PendingIntent pendingIntent = PendingIntent.getBroadcast( 416 getContext(), 0, updateIntent, 0); 417 mAlarmManager.cancel(pendingIntent); 418 mAlarmManager.setExact(AlarmManager.RTC, nextUpdate, pendingIntent); 419 } 420 } 421 422 private final BroadcastReceiver mUpdateLocationReceiver = new BroadcastReceiver() { 423 @Override 424 public void onReceive(Context context, Intent intent) { 425 if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction()) 426 && !intent.getBooleanExtra("state", false)) { 427 // Airplane mode is now off! 428 mLocationHandler.requestLocationUpdate(); 429 return; 430 } 431 432 // Time zone has changed or alarm expired. 433 mLocationHandler.requestTwilightUpdate(); 434 } 435 }; 436 437 // A LocationListener to initialize the network location provider. The location updates 438 // are handled through the passive location provider. 439 private final LocationListener mEmptyLocationListener = new LocationListener() { 440 public void onLocationChanged(Location location) { 441 } 442 443 public void onProviderDisabled(String provider) { 444 } 445 446 public void onProviderEnabled(String provider) { 447 } 448 449 public void onStatusChanged(String provider, int status, Bundle extras) { 450 } 451 }; 452 453 private final LocationListener mLocationListener = new LocationListener() { 454 public void onLocationChanged(Location location) { 455 mLocationHandler.processNewLocation(location); 456 } 457 458 public void onProviderDisabled(String provider) { 459 } 460 461 public void onProviderEnabled(String provider) { 462 } 463 464 public void onStatusChanged(String provider, int status, Bundle extras) { 465 } 466 }; 467} 468