Home | History | Annotate | Download | only in twilight
      1 /*
      2  * Copyright (C) 2016 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 android.annotation.NonNull;
     20 import android.app.AlarmManager;
     21 import android.content.BroadcastReceiver;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.icu.impl.CalendarAstronomer;
     26 import android.icu.util.Calendar;
     27 import android.location.Location;
     28 import android.location.LocationListener;
     29 import android.location.LocationManager;
     30 import android.os.Bundle;
     31 import android.os.Handler;
     32 import android.os.Looper;
     33 import android.os.Message;
     34 import android.util.ArrayMap;
     35 import android.util.Slog;
     36 
     37 import com.android.internal.annotations.GuardedBy;
     38 import com.android.server.SystemService;
     39 
     40 import java.util.Objects;
     41 
     42 /**
     43  * Figures out whether it's twilight time based on the user's location.
     44  * <p>
     45  * Used by the UI mode manager and other components to adjust night mode
     46  * effects based on sunrise and sunset.
     47  */
     48 public final class TwilightService extends SystemService
     49         implements AlarmManager.OnAlarmListener, Handler.Callback, LocationListener {
     50 
     51     private static final String TAG = "TwilightService";
     52     private static final boolean DEBUG = false;
     53 
     54     private static final int MSG_START_LISTENING = 1;
     55     private static final int MSG_STOP_LISTENING = 2;
     56 
     57     @GuardedBy("mListeners")
     58     private final ArrayMap<TwilightListener, Handler> mListeners = new ArrayMap<>();
     59 
     60     private final Handler mHandler;
     61 
     62     protected AlarmManager mAlarmManager;
     63     private LocationManager mLocationManager;
     64 
     65     private boolean mBootCompleted;
     66     private boolean mHasListeners;
     67 
     68     private BroadcastReceiver mTimeChangedReceiver;
     69     protected Location mLastLocation;
     70 
     71     @GuardedBy("mListeners")
     72     protected TwilightState mLastTwilightState;
     73 
     74     public TwilightService(Context context) {
     75         super(context);
     76         mHandler = new Handler(Looper.getMainLooper(), this);
     77     }
     78 
     79     @Override
     80     public void onStart() {
     81         publishLocalService(TwilightManager.class, new TwilightManager() {
     82             @Override
     83             public void registerListener(@NonNull TwilightListener listener,
     84                     @NonNull Handler handler) {
     85                 synchronized (mListeners) {
     86                     final boolean wasEmpty = mListeners.isEmpty();
     87                     mListeners.put(listener, handler);
     88 
     89                     if (wasEmpty && !mListeners.isEmpty()) {
     90                         mHandler.sendEmptyMessage(MSG_START_LISTENING);
     91                     }
     92                 }
     93             }
     94 
     95             @Override
     96             public void unregisterListener(@NonNull TwilightListener listener) {
     97                 synchronized (mListeners) {
     98                     final boolean wasEmpty = mListeners.isEmpty();
     99                     mListeners.remove(listener);
    100 
    101                     if (!wasEmpty && mListeners.isEmpty()) {
    102                         mHandler.sendEmptyMessage(MSG_STOP_LISTENING);
    103                     }
    104                 }
    105             }
    106 
    107             @Override
    108             public TwilightState getLastTwilightState() {
    109                 synchronized (mListeners) {
    110                     return mLastTwilightState;
    111                 }
    112             }
    113         });
    114     }
    115 
    116     @Override
    117     public void onBootPhase(int phase) {
    118         if (phase == PHASE_BOOT_COMPLETED) {
    119             final Context c = getContext();
    120             mAlarmManager = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE);
    121             mLocationManager = (LocationManager) c.getSystemService(Context.LOCATION_SERVICE);
    122 
    123             mBootCompleted = true;
    124             if (mHasListeners) {
    125                 startListening();
    126             }
    127         }
    128     }
    129 
    130     @Override
    131     public boolean handleMessage(Message msg) {
    132         switch (msg.what) {
    133             case MSG_START_LISTENING:
    134                 if (!mHasListeners) {
    135                     mHasListeners = true;
    136                     if (mBootCompleted) {
    137                         startListening();
    138                     }
    139                 }
    140                 return true;
    141             case MSG_STOP_LISTENING:
    142                 if (mHasListeners) {
    143                     mHasListeners = false;
    144                     if (mBootCompleted) {
    145                         stopListening();
    146                     }
    147                 }
    148                 return true;
    149         }
    150         return false;
    151     }
    152 
    153     private void startListening() {
    154         Slog.d(TAG, "startListening");
    155 
    156         // Start listening for location updates (default: low power, max 1h, min 10m).
    157         mLocationManager.requestLocationUpdates(
    158                 null /* default */, this, Looper.getMainLooper());
    159 
    160         // Request the device's location immediately if a previous location isn't available.
    161         if (mLocationManager.getLastLocation() == null) {
    162             if (mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
    163                 mLocationManager.requestSingleUpdate(
    164                         LocationManager.NETWORK_PROVIDER, this, Looper.getMainLooper());
    165             }
    166         }
    167 
    168         // Update whenever the system clock is changed.
    169         if (mTimeChangedReceiver == null) {
    170             mTimeChangedReceiver = new BroadcastReceiver() {
    171                 @Override
    172                 public void onReceive(Context context, Intent intent) {
    173                     Slog.d(TAG, "onReceive: " + intent);
    174                     updateTwilightState();
    175                 }
    176             };
    177 
    178             final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED);
    179             intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
    180             getContext().registerReceiver(mTimeChangedReceiver, intentFilter);
    181         }
    182 
    183         // Force an update now that we have listeners registered.
    184         updateTwilightState();
    185     }
    186 
    187     private void stopListening() {
    188         Slog.d(TAG, "stopListening");
    189 
    190         if (mTimeChangedReceiver != null) {
    191             getContext().unregisterReceiver(mTimeChangedReceiver);
    192             mTimeChangedReceiver = null;
    193         }
    194 
    195         if (mLastTwilightState != null) {
    196             mAlarmManager.cancel(this);
    197         }
    198 
    199         mLocationManager.removeUpdates(this);
    200         mLastLocation = null;
    201     }
    202 
    203     private void updateTwilightState() {
    204         // Calculate the twilight state based on the current time and location.
    205         final long currentTimeMillis = System.currentTimeMillis();
    206         final Location location = mLastLocation != null ? mLastLocation
    207                 : mLocationManager.getLastLocation();
    208         final TwilightState state = calculateTwilightState(location, currentTimeMillis);
    209         if (DEBUG) {
    210             Slog.d(TAG, "updateTwilightState: " + state);
    211         }
    212 
    213         // Notify listeners if the state has changed.
    214         synchronized (mListeners) {
    215             if (!Objects.equals(mLastTwilightState, state)) {
    216                 mLastTwilightState = state;
    217 
    218                 for (int i = mListeners.size() - 1; i >= 0; --i) {
    219                     final TwilightListener listener = mListeners.keyAt(i);
    220                     final Handler handler = mListeners.valueAt(i);
    221                     handler.post(new Runnable() {
    222                         @Override
    223                         public void run() {
    224                             listener.onTwilightStateChanged(state);
    225                         }
    226                     });
    227                 }
    228             }
    229         }
    230 
    231         // Schedule an alarm to update the state at the next sunrise or sunset.
    232         if (state != null) {
    233             final long triggerAtMillis = state.isNight()
    234                     ? state.sunriseTimeMillis() : state.sunsetTimeMillis();
    235             mAlarmManager.setExact(AlarmManager.RTC, triggerAtMillis, TAG, this, mHandler);
    236         }
    237     }
    238 
    239     @Override
    240     public void onAlarm() {
    241         Slog.d(TAG, "onAlarm");
    242         updateTwilightState();
    243     }
    244 
    245     @Override
    246     public void onLocationChanged(Location location) {
    247         // Location providers may erroneously return (0.0, 0.0) when they fail to determine the
    248         // device's location. These location updates can be safely ignored since the chance of a
    249         // user actually being at these coordinates is quite low.
    250         if (location != null
    251                 && !(location.getLongitude() == 0.0 && location.getLatitude() == 0.0)) {
    252             Slog.d(TAG, "onLocationChanged:"
    253                     + " provider=" + location.getProvider()
    254                     + " accuracy=" + location.getAccuracy()
    255                     + " time=" + location.getTime());
    256             mLastLocation = location;
    257             updateTwilightState();
    258         }
    259     }
    260 
    261     @Override
    262     public void onStatusChanged(String provider, int status, Bundle extras) {
    263     }
    264 
    265     @Override
    266     public void onProviderEnabled(String provider) {
    267     }
    268 
    269     @Override
    270     public void onProviderDisabled(String provider) {
    271     }
    272 
    273     /**
    274      * Calculates the twilight state for a specific location and time.
    275      *
    276      * @param location the location to use
    277      * @param timeMillis the reference time to use
    278      * @return the calculated {@link TwilightState}, or {@code null} if location is {@code null}
    279      */
    280     private static TwilightState calculateTwilightState(Location location, long timeMillis) {
    281         if (location == null) {
    282             return null;
    283         }
    284 
    285         final CalendarAstronomer ca = new CalendarAstronomer(
    286                 location.getLongitude(), location.getLatitude());
    287 
    288         final Calendar noon = Calendar.getInstance();
    289         noon.setTimeInMillis(timeMillis);
    290         noon.set(Calendar.HOUR_OF_DAY, 12);
    291         noon.set(Calendar.MINUTE, 0);
    292         noon.set(Calendar.SECOND, 0);
    293         noon.set(Calendar.MILLISECOND, 0);
    294         ca.setTime(noon.getTimeInMillis());
    295 
    296         long sunriseTimeMillis = ca.getSunRiseSet(true /* rise */);
    297         long sunsetTimeMillis = ca.getSunRiseSet(false /* rise */);
    298 
    299         if (sunsetTimeMillis < timeMillis) {
    300             noon.add(Calendar.DATE, 1);
    301             ca.setTime(noon.getTimeInMillis());
    302             sunriseTimeMillis = ca.getSunRiseSet(true /* rise */);
    303         } else if (sunriseTimeMillis > timeMillis) {
    304             noon.add(Calendar.DATE, -1);
    305             ca.setTime(noon.getTimeInMillis());
    306             sunsetTimeMillis = ca.getSunRiseSet(false /* rise */);
    307         }
    308 
    309         return new TwilightState(sunriseTimeMillis, sunsetTimeMillis);
    310     }
    311 }
    312