Home | History | Annotate | Download | only in twilight
      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