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