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 17package com.android.server.twilight; 18 19import android.annotation.NonNull; 20import android.app.AlarmManager; 21import android.content.BroadcastReceiver; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.icu.impl.CalendarAstronomer; 26import android.icu.util.Calendar; 27import android.location.Location; 28import android.location.LocationListener; 29import android.location.LocationManager; 30import android.os.Bundle; 31import android.os.Handler; 32import android.os.Looper; 33import android.os.Message; 34import android.util.ArrayMap; 35import android.util.Slog; 36 37import com.android.internal.annotations.GuardedBy; 38import com.android.server.SystemService; 39 40import 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 */ 48public 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 } else if (mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { 166 mLocationManager.requestSingleUpdate( 167 LocationManager.GPS_PROVIDER, this, Looper.getMainLooper()); 168 } 169 } 170 171 // Update whenever the system clock is changed. 172 if (mTimeChangedReceiver == null) { 173 mTimeChangedReceiver = new BroadcastReceiver() { 174 @Override 175 public void onReceive(Context context, Intent intent) { 176 Slog.d(TAG, "onReceive: " + intent); 177 updateTwilightState(); 178 } 179 }; 180 181 final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED); 182 intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 183 getContext().registerReceiver(mTimeChangedReceiver, intentFilter); 184 } 185 186 // Force an update now that we have listeners registered. 187 updateTwilightState(); 188 } 189 190 private void stopListening() { 191 Slog.d(TAG, "stopListening"); 192 193 if (mTimeChangedReceiver != null) { 194 getContext().unregisterReceiver(mTimeChangedReceiver); 195 mTimeChangedReceiver = null; 196 } 197 198 if (mLastTwilightState != null) { 199 mAlarmManager.cancel(this); 200 } 201 202 mLocationManager.removeUpdates(this); 203 mLastLocation = null; 204 } 205 206 private void updateTwilightState() { 207 // Calculate the twilight state based on the current time and location. 208 final long currentTimeMillis = System.currentTimeMillis(); 209 final Location location = mLastLocation != null ? mLastLocation 210 : mLocationManager.getLastLocation(); 211 final TwilightState state = calculateTwilightState(location, currentTimeMillis); 212 if (DEBUG) { 213 Slog.d(TAG, "updateTwilightState: " + state); 214 } 215 216 // Notify listeners if the state has changed. 217 synchronized (mListeners) { 218 if (!Objects.equals(mLastTwilightState, state)) { 219 mLastTwilightState = state; 220 221 for (int i = mListeners.size() - 1; i >= 0; --i) { 222 final TwilightListener listener = mListeners.keyAt(i); 223 final Handler handler = mListeners.valueAt(i); 224 handler.post(new Runnable() { 225 @Override 226 public void run() { 227 listener.onTwilightStateChanged(state); 228 } 229 }); 230 } 231 } 232 } 233 234 // Schedule an alarm to update the state at the next sunrise or sunset. 235 if (state != null) { 236 final long triggerAtMillis = state.isNight() 237 ? state.sunriseTimeMillis() : state.sunsetTimeMillis(); 238 mAlarmManager.setExact(AlarmManager.RTC, triggerAtMillis, TAG, this, mHandler); 239 } 240 } 241 242 @Override 243 public void onAlarm() { 244 Slog.d(TAG, "onAlarm"); 245 updateTwilightState(); 246 } 247 248 @Override 249 public void onLocationChanged(Location location) { 250 // Location providers may erroneously return (0.0, 0.0) when they fail to determine the 251 // device's location. These location updates can be safely ignored since the chance of a 252 // user actually being at these coordinates is quite low. 253 if (location != null 254 && !(location.getLongitude() == 0.0 && location.getLatitude() == 0.0)) { 255 Slog.d(TAG, "onLocationChanged:" 256 + " provider=" + location.getProvider() 257 + " accuracy=" + location.getAccuracy() 258 + " time=" + location.getTime()); 259 mLastLocation = location; 260 updateTwilightState(); 261 } 262 } 263 264 @Override 265 public void onStatusChanged(String provider, int status, Bundle extras) { 266 } 267 268 @Override 269 public void onProviderEnabled(String provider) { 270 } 271 272 @Override 273 public void onProviderDisabled(String provider) { 274 } 275 276 /** 277 * Calculates the twilight state for a specific location and time. 278 * 279 * @param location the location to use 280 * @param timeMillis the reference time to use 281 * @return the calculated {@link TwilightState}, or {@code null} if location is {@code null} 282 */ 283 private static TwilightState calculateTwilightState(Location location, long timeMillis) { 284 if (location == null) { 285 return null; 286 } 287 288 final CalendarAstronomer ca = new CalendarAstronomer( 289 location.getLongitude(), location.getLatitude()); 290 291 final Calendar noon = Calendar.getInstance(); 292 noon.setTimeInMillis(timeMillis); 293 noon.set(Calendar.HOUR_OF_DAY, 12); 294 noon.set(Calendar.MINUTE, 0); 295 noon.set(Calendar.SECOND, 0); 296 noon.set(Calendar.MILLISECOND, 0); 297 ca.setTime(noon.getTimeInMillis()); 298 299 long sunriseTimeMillis = ca.getSunRiseSet(true /* rise */); 300 long sunsetTimeMillis = ca.getSunRiseSet(false /* rise */); 301 302 if (sunsetTimeMillis < timeMillis) { 303 noon.add(Calendar.DATE, 1); 304 ca.setTime(noon.getTimeInMillis()); 305 sunriseTimeMillis = ca.getSunRiseSet(true /* rise */); 306 } else if (sunriseTimeMillis > timeMillis) { 307 noon.add(Calendar.DATE, -1); 308 ca.setTime(noon.getTimeInMillis()); 309 sunsetTimeMillis = ca.getSunRiseSet(false /* rise */); 310 } 311 312 return new TwilightState(sunriseTimeMillis, sunsetTimeMillis); 313 } 314} 315