Home | History | Annotate | Download | only in server
      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;
     18 
     19 import android.app.AlarmManager;
     20 import android.app.PendingIntent;
     21 import android.content.BroadcastReceiver;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.location.Criteria;
     26 import android.location.Location;
     27 import android.location.LocationListener;
     28 import android.location.LocationManager;
     29 import android.os.Bundle;
     30 import android.os.Handler;
     31 import android.os.Message;
     32 import android.os.SystemClock;
     33 import android.text.format.DateUtils;
     34 import android.text.format.Time;
     35 import android.util.Slog;
     36 
     37 import java.text.DateFormat;
     38 import java.util.ArrayList;
     39 import java.util.Date;
     40 import java.util.Iterator;
     41 
     42 import 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  */
     50 public 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.setExact(AlarmManager.RTC, 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