TwilightService.java revision 4118012da9a22694b3353040a485f8cdc27e2f17
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;
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
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