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