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 17 package com.android.server; 18 19 import android.app.AlarmManager; 20 import android.app.PendingIntent; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.location.Criteria; 26 import android.location.Location; 27 import android.location.LocationListener; 28 import android.location.LocationManager; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.os.Message; 32 import android.os.SystemClock; 33 import android.text.format.DateUtils; 34 import android.text.format.Time; 35 import android.util.Slog; 36 37 import java.text.DateFormat; 38 import java.util.ArrayList; 39 import java.util.Date; 40 import java.util.Iterator; 41 42 import libcore.util.Objects; 43 44 /** 45 * Figures out whether it's twilight time based on the user's location. 46 * 47 * Used by the UI mode manager and other components to adjust night mode 48 * effects based on sunrise and sunset. 49 */ 50 public final class TwilightService { 51 private static final String TAG = "TwilightService"; 52 53 private static final boolean DEBUG = false; 54 55 private static final String ACTION_UPDATE_TWILIGHT_STATE = 56 "com.android.server.action.UPDATE_TWILIGHT_STATE"; 57 58 private final Context mContext; 59 private final AlarmManager mAlarmManager; 60 private final LocationManager mLocationManager; 61 private final LocationHandler mLocationHandler; 62 63 private final Object mLock = new Object(); 64 65 private final ArrayList<TwilightListenerRecord> mListeners = 66 new ArrayList<TwilightListenerRecord>(); 67 68 private boolean mSystemReady; 69 70 private TwilightState mTwilightState; 71 72 public TwilightService(Context context) { 73 mContext = context; 74 75 mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); 76 mLocationManager = (LocationManager)mContext.getSystemService(Context.LOCATION_SERVICE); 77 mLocationHandler = new LocationHandler(); 78 } 79 80 void systemReady() { 81 synchronized (mLock) { 82 mSystemReady = true; 83 84 IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); 85 filter.addAction(Intent.ACTION_TIME_CHANGED); 86 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 87 filter.addAction(ACTION_UPDATE_TWILIGHT_STATE); 88 mContext.registerReceiver(mUpdateLocationReceiver, filter); 89 90 if (!mListeners.isEmpty()) { 91 mLocationHandler.enableLocationUpdates(); 92 } 93 } 94 } 95 96 /** 97 * Gets the current twilight state. 98 * 99 * @return The current twilight state, or null if no information is available. 100 */ 101 public TwilightState getCurrentState() { 102 synchronized (mLock) { 103 return mTwilightState; 104 } 105 } 106 107 /** 108 * Listens for twilight time. 109 * 110 * @param listener The listener. 111 * @param handler The handler on which to post calls into the listener. 112 */ 113 public void registerListener(TwilightListener listener, Handler handler) { 114 synchronized (mLock) { 115 mListeners.add(new TwilightListenerRecord(listener, handler)); 116 117 if (mSystemReady && mListeners.size() == 1) { 118 mLocationHandler.enableLocationUpdates(); 119 } 120 } 121 } 122 123 private void setTwilightState(TwilightState state) { 124 synchronized (mLock) { 125 if (!Objects.equal(mTwilightState, state)) { 126 if (DEBUG) { 127 Slog.d(TAG, "Twilight state changed: " + state); 128 } 129 130 mTwilightState = state; 131 int count = mListeners.size(); 132 for (int i = 0; i < count; i++) { 133 mListeners.get(i).post(); 134 } 135 } 136 } 137 } 138 139 // The user has moved if the accuracy circles of the two locations don't overlap. 140 private static boolean hasMoved(Location from, Location to) { 141 if (to == null) { 142 return false; 143 } 144 145 if (from == null) { 146 return true; 147 } 148 149 // if new location is older than the current one, the device hasn't moved. 150 if (to.getElapsedRealtimeNanos() < from.getElapsedRealtimeNanos()) { 151 return false; 152 } 153 154 // Get the distance between the two points. 155 float distance = from.distanceTo(to); 156 157 // Get the total accuracy radius for both locations. 158 float totalAccuracy = from.getAccuracy() + to.getAccuracy(); 159 160 // If the distance is greater than the combined accuracy of the two 161 // points then they can't overlap and hence the user has moved. 162 return distance >= totalAccuracy; 163 } 164 165 /** 166 * Describes whether it is day or night. 167 * This object is immutable. 168 */ 169 public static final class TwilightState { 170 private final boolean mIsNight; 171 private final long mYesterdaySunset; 172 private final long mTodaySunrise; 173 private final long mTodaySunset; 174 private final long mTomorrowSunrise; 175 176 TwilightState(boolean isNight, 177 long yesterdaySunset, 178 long todaySunrise, long todaySunset, 179 long tomorrowSunrise) { 180 mIsNight = isNight; 181 mYesterdaySunset = yesterdaySunset; 182 mTodaySunrise = todaySunrise; 183 mTodaySunset = todaySunset; 184 mTomorrowSunrise = tomorrowSunrise; 185 } 186 187 /** 188 * Returns true if it is currently night time. 189 */ 190 public boolean isNight() { 191 return mIsNight; 192 } 193 194 /** 195 * Returns the time of yesterday's sunset in the System.currentTimeMillis() timebase, 196 * or -1 if the sun never sets. 197 */ 198 public long getYesterdaySunset() { 199 return mYesterdaySunset; 200 } 201 202 /** 203 * Returns the time of today's sunrise in the System.currentTimeMillis() timebase, 204 * or -1 if the sun never rises. 205 */ 206 public long getTodaySunrise() { 207 return mTodaySunrise; 208 } 209 210 /** 211 * Returns the time of today's sunset in the System.currentTimeMillis() timebase, 212 * or -1 if the sun never sets. 213 */ 214 public long getTodaySunset() { 215 return mTodaySunset; 216 } 217 218 /** 219 * Returns the time of tomorrow's sunrise in the System.currentTimeMillis() timebase, 220 * or -1 if the sun never rises. 221 */ 222 public long getTomorrowSunrise() { 223 return mTomorrowSunrise; 224 } 225 226 @Override 227 public boolean equals(Object o) { 228 return o instanceof TwilightState && equals((TwilightState)o); 229 } 230 231 public boolean equals(TwilightState other) { 232 return other != null 233 && mIsNight == other.mIsNight 234 && mYesterdaySunset == other.mYesterdaySunset 235 && mTodaySunrise == other.mTodaySunrise 236 && mTodaySunset == other.mTodaySunset 237 && mTomorrowSunrise == other.mTomorrowSunrise; 238 } 239 240 @Override 241 public int hashCode() { 242 return 0; // don't care 243 } 244 245 @Override 246 public String toString() { 247 DateFormat f = DateFormat.getDateTimeInstance(); 248 return "{TwilightState: isNight=" + mIsNight 249 + ", mYesterdaySunset=" + f.format(new Date(mYesterdaySunset)) 250 + ", mTodaySunrise=" + f.format(new Date(mTodaySunrise)) 251 + ", mTodaySunset=" + f.format(new Date(mTodaySunset)) 252 + ", mTomorrowSunrise=" + f.format(new Date(mTomorrowSunrise)) 253 + "}"; 254 } 255 } 256 257 /** 258 * Listener for changes in twilight state. 259 */ 260 public interface TwilightListener { 261 public void onTwilightStateChanged(); 262 } 263 264 private static final class TwilightListenerRecord implements Runnable { 265 private final TwilightListener mListener; 266 private final Handler mHandler; 267 268 public TwilightListenerRecord(TwilightListener listener, Handler handler) { 269 mListener = listener; 270 mHandler = handler; 271 } 272 273 public void post() { 274 mHandler.post(this); 275 } 276 277 @Override 278 public void run() { 279 mListener.onTwilightStateChanged(); 280 } 281 } 282 283 private final class LocationHandler extends Handler { 284 private static final int MSG_ENABLE_LOCATION_UPDATES = 1; 285 private static final int MSG_GET_NEW_LOCATION_UPDATE = 2; 286 private static final int MSG_PROCESS_NEW_LOCATION = 3; 287 private static final int MSG_DO_TWILIGHT_UPDATE = 4; 288 289 private static final long LOCATION_UPDATE_MS = 24 * DateUtils.HOUR_IN_MILLIS; 290 private static final long MIN_LOCATION_UPDATE_MS = 30 * DateUtils.MINUTE_IN_MILLIS; 291 private static final float LOCATION_UPDATE_DISTANCE_METER = 1000 * 20; 292 private static final long LOCATION_UPDATE_ENABLE_INTERVAL_MIN = 5000; 293 private static final long LOCATION_UPDATE_ENABLE_INTERVAL_MAX = 294 15 * DateUtils.MINUTE_IN_MILLIS; 295 private static final double FACTOR_GMT_OFFSET_LONGITUDE = 296 1000.0 * 360.0 / DateUtils.DAY_IN_MILLIS; 297 298 private boolean mPassiveListenerEnabled; 299 private boolean mNetworkListenerEnabled; 300 private boolean mDidFirstInit; 301 private long mLastNetworkRegisterTime = -MIN_LOCATION_UPDATE_MS; 302 private long mLastUpdateInterval; 303 private Location mLocation; 304 private final TwilightCalculator mTwilightCalculator = new TwilightCalculator(); 305 306 public void processNewLocation(Location location) { 307 Message msg = obtainMessage(MSG_PROCESS_NEW_LOCATION, location); 308 sendMessage(msg); 309 } 310 311 public void enableLocationUpdates() { 312 sendEmptyMessage(MSG_ENABLE_LOCATION_UPDATES); 313 } 314 315 public void requestLocationUpdate() { 316 sendEmptyMessage(MSG_GET_NEW_LOCATION_UPDATE); 317 } 318 319 public void requestTwilightUpdate() { 320 sendEmptyMessage(MSG_DO_TWILIGHT_UPDATE); 321 } 322 323 @Override 324 public void handleMessage(Message msg) { 325 switch (msg.what) { 326 case MSG_PROCESS_NEW_LOCATION: { 327 final Location location = (Location)msg.obj; 328 final boolean hasMoved = hasMoved(mLocation, location); 329 final boolean hasBetterAccuracy = mLocation == null 330 || location.getAccuracy() < mLocation.getAccuracy(); 331 if (DEBUG) { 332 Slog.d(TAG, "Processing new location: " + location 333 + ", hasMoved=" + hasMoved 334 + ", hasBetterAccuracy=" + hasBetterAccuracy); 335 } 336 if (hasMoved || hasBetterAccuracy) { 337 setLocation(location); 338 } 339 break; 340 } 341 342 case MSG_GET_NEW_LOCATION_UPDATE: 343 if (!mNetworkListenerEnabled) { 344 // Don't do anything -- we are still trying to get a 345 // location. 346 return; 347 } 348 if ((mLastNetworkRegisterTime + MIN_LOCATION_UPDATE_MS) >= 349 SystemClock.elapsedRealtime()) { 350 // Don't do anything -- it hasn't been long enough 351 // since we last requested an update. 352 return; 353 } 354 355 // Unregister the current location monitor, so we can 356 // register a new one for it to get an immediate update. 357 mNetworkListenerEnabled = false; 358 mLocationManager.removeUpdates(mEmptyLocationListener); 359 360 // Fall through to re-register listener. 361 case MSG_ENABLE_LOCATION_UPDATES: 362 // enable network provider to receive at least location updates for a given 363 // distance. 364 boolean networkLocationEnabled; 365 try { 366 networkLocationEnabled = 367 mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER); 368 } catch (Exception e) { 369 // we may get IllegalArgumentException if network location provider 370 // does not exist or is not yet installed. 371 networkLocationEnabled = false; 372 } 373 if (!mNetworkListenerEnabled && networkLocationEnabled) { 374 mNetworkListenerEnabled = true; 375 mLastNetworkRegisterTime = SystemClock.elapsedRealtime(); 376 mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 377 LOCATION_UPDATE_MS, 0, mEmptyLocationListener); 378 379 if (!mDidFirstInit) { 380 mDidFirstInit = true; 381 if (mLocation == null) { 382 retrieveLocation(); 383 } 384 } 385 } 386 387 // enable passive provider to receive updates from location fixes (gps 388 // and network). 389 boolean passiveLocationEnabled; 390 try { 391 passiveLocationEnabled = 392 mLocationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER); 393 } catch (Exception e) { 394 // we may get IllegalArgumentException if passive location provider 395 // does not exist or is not yet installed. 396 passiveLocationEnabled = false; 397 } 398 399 if (!mPassiveListenerEnabled && passiveLocationEnabled) { 400 mPassiveListenerEnabled = true; 401 mLocationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 402 0, LOCATION_UPDATE_DISTANCE_METER , mLocationListener); 403 } 404 405 if (!(mNetworkListenerEnabled && mPassiveListenerEnabled)) { 406 mLastUpdateInterval *= 1.5; 407 if (mLastUpdateInterval == 0) { 408 mLastUpdateInterval = LOCATION_UPDATE_ENABLE_INTERVAL_MIN; 409 } else if (mLastUpdateInterval > LOCATION_UPDATE_ENABLE_INTERVAL_MAX) { 410 mLastUpdateInterval = LOCATION_UPDATE_ENABLE_INTERVAL_MAX; 411 } 412 sendEmptyMessageDelayed(MSG_ENABLE_LOCATION_UPDATES, mLastUpdateInterval); 413 } 414 break; 415 416 case MSG_DO_TWILIGHT_UPDATE: 417 updateTwilightState(); 418 break; 419 } 420 } 421 422 private void retrieveLocation() { 423 Location location = null; 424 final Iterator<String> providers = 425 mLocationManager.getProviders(new Criteria(), true).iterator(); 426 while (providers.hasNext()) { 427 final Location lastKnownLocation = 428 mLocationManager.getLastKnownLocation(providers.next()); 429 // pick the most recent location 430 if (location == null || (lastKnownLocation != null && 431 location.getElapsedRealtimeNanos() < 432 lastKnownLocation.getElapsedRealtimeNanos())) { 433 location = lastKnownLocation; 434 } 435 } 436 437 // In the case there is no location available (e.g. GPS fix or network location 438 // is not available yet), the longitude of the location is estimated using the timezone, 439 // latitude and accuracy are set to get a good average. 440 if (location == null) { 441 Time currentTime = new Time(); 442 currentTime.set(System.currentTimeMillis()); 443 double lngOffset = FACTOR_GMT_OFFSET_LONGITUDE * 444 (currentTime.gmtoff - (currentTime.isDst > 0 ? 3600 : 0)); 445 location = new Location("fake"); 446 location.setLongitude(lngOffset); 447 location.setLatitude(0); 448 location.setAccuracy(417000.0f); 449 location.setTime(System.currentTimeMillis()); 450 location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos()); 451 452 if (DEBUG) { 453 Slog.d(TAG, "Estimated location from timezone: " + location); 454 } 455 } 456 457 setLocation(location); 458 } 459 460 private void setLocation(Location location) { 461 mLocation = location; 462 updateTwilightState(); 463 } 464 465 private void updateTwilightState() { 466 if (mLocation == null) { 467 setTwilightState(null); 468 return; 469 } 470 471 final long now = System.currentTimeMillis(); 472 473 // calculate yesterday's twilight 474 mTwilightCalculator.calculateTwilight(now - DateUtils.DAY_IN_MILLIS, 475 mLocation.getLatitude(), mLocation.getLongitude()); 476 final long yesterdaySunset = mTwilightCalculator.mSunset; 477 478 // calculate today's twilight 479 mTwilightCalculator.calculateTwilight(now, 480 mLocation.getLatitude(), mLocation.getLongitude()); 481 final boolean isNight = (mTwilightCalculator.mState == TwilightCalculator.NIGHT); 482 final long todaySunrise = mTwilightCalculator.mSunrise; 483 final long todaySunset = mTwilightCalculator.mSunset; 484 485 // calculate tomorrow's twilight 486 mTwilightCalculator.calculateTwilight(now + DateUtils.DAY_IN_MILLIS, 487 mLocation.getLatitude(), mLocation.getLongitude()); 488 final long tomorrowSunrise = mTwilightCalculator.mSunrise; 489 490 // set twilight state 491 TwilightState state = new TwilightState(isNight, yesterdaySunset, 492 todaySunrise, todaySunset, tomorrowSunrise); 493 if (DEBUG) { 494 Slog.d(TAG, "Updating twilight state: " + state); 495 } 496 setTwilightState(state); 497 498 // schedule next update 499 long nextUpdate = 0; 500 if (todaySunrise == -1 || todaySunset == -1) { 501 // In the case the day or night never ends the update is scheduled 12 hours later. 502 nextUpdate = now + 12 * DateUtils.HOUR_IN_MILLIS; 503 } else { 504 // add some extra time to be on the safe side. 505 nextUpdate += DateUtils.MINUTE_IN_MILLIS; 506 507 if (now > todaySunset) { 508 nextUpdate += tomorrowSunrise; 509 } else if (now > todaySunrise) { 510 nextUpdate += todaySunset; 511 } else { 512 nextUpdate += todaySunrise; 513 } 514 } 515 516 if (DEBUG) { 517 Slog.d(TAG, "Next update in " + (nextUpdate - now) + " ms"); 518 } 519 520 Intent updateIntent = new Intent(ACTION_UPDATE_TWILIGHT_STATE); 521 PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, updateIntent, 0); 522 mAlarmManager.cancel(pendingIntent); 523 mAlarmManager.set(AlarmManager.RTC_WAKEUP, nextUpdate, pendingIntent); 524 } 525 }; 526 527 private final BroadcastReceiver mUpdateLocationReceiver = new BroadcastReceiver() { 528 @Override 529 public void onReceive(Context context, Intent intent) { 530 if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction()) 531 && !intent.getBooleanExtra("state", false)) { 532 // Airplane mode is now off! 533 mLocationHandler.requestLocationUpdate(); 534 return; 535 } 536 537 // Time zone has changed or alarm expired. 538 mLocationHandler.requestTwilightUpdate(); 539 } 540 }; 541 542 // A LocationListener to initialize the network location provider. The location updates 543 // are handled through the passive location provider. 544 private final LocationListener mEmptyLocationListener = new LocationListener() { 545 public void onLocationChanged(Location location) { 546 } 547 548 public void onProviderDisabled(String provider) { 549 } 550 551 public void onProviderEnabled(String provider) { 552 } 553 554 public void onStatusChanged(String provider, int status, Bundle extras) { 555 } 556 }; 557 558 private final LocationListener mLocationListener = new LocationListener() { 559 public void onLocationChanged(Location location) { 560 mLocationHandler.processNewLocation(location); 561 } 562 563 public void onProviderDisabled(String provider) { 564 } 565 566 public void onProviderEnabled(String provider) { 567 } 568 569 public void onStatusChanged(String provider, int status, Bundle extras) { 570 } 571 }; 572 } 573