blob: 154de1c34103af514b25911241936ff1853505e5 [file] [log] [blame]
Jeff Brown2416e092012-08-21 22:12:20 -07001/*
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;
18
19import android.app.AlarmManager;
20import android.app.PendingIntent;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.location.Criteria;
26import android.location.Location;
27import android.location.LocationListener;
28import android.location.LocationManager;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.Message;
32import android.os.SystemClock;
33import android.text.format.DateUtils;
34import android.text.format.Time;
35import android.util.Slog;
36
37import java.text.DateFormat;
38import java.util.ArrayList;
39import java.util.Date;
40import java.util.Iterator;
41
42import 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 */
50public final class TwilightService {
51 private static final String TAG = "TwilightService";
52
Jeff Brownb5b710a2012-08-22 13:12:48 -070053 private static final boolean DEBUG = false;
Jeff Brown2416e092012-08-21 22:12:20 -070054
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.
Philip Milne41180122012-09-26 11:29:25 -0700150 if (to.getElapsedRealtimeNanos() < from.getElapsedRealtimeNanos()) {
Jeff Brown2416e092012-08-21 22:12:20 -0700151 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 &&
Philip Milne41180122012-09-26 11:29:25 -0700431 location.getElapsedRealtimeNanos() <
432 lastKnownLocation.getElapsedRealtimeNanos())) {
Jeff Brown2416e092012-08-21 22:12:20 -0700433 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());
Philip Milne41180122012-09-26 11:29:25 -0700450 location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
Jeff Brown2416e092012-08-21 22:12:20 -0700451
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() {
Jeff Brownb5b710a2012-08-22 13:12:48 -0700466 if (mLocation == null) {
467 setTwilightState(null);
468 return;
469 }
470
Jeff Brown2416e092012-08-21 22:12:20 -0700471 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}