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