TwilightService.java revision b880d880c6cd989eacc28c365fc9a41d31900da1
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    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