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 private AlarmManager mAlarmManager; 63 private LocationManager mLocationManager; 64 65 private boolean mBootCompleted; 66 private boolean mHasListeners; 67 68 private BroadcastReceiver mTimeChangedReceiver; 69 private Location mLastLocation; 70 71 @GuardedBy("mListeners") 72 private 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 if (DEBUG) 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 if (DEBUG) 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 if (DEBUG) 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 if (DEBUG) Slog.d(TAG, "onAlarm"); 245 updateTwilightState(); 246 } 247 248 @Override 249 public void onLocationChanged(Location location) { 250 if (DEBUG) Slog.d(TAG, "onLocationChanged: " + location); 251 mLastLocation = location; 252 updateTwilightState(); 253 } 254 255 @Override 256 public void onStatusChanged(String provider, int status, Bundle extras) { 257 } 258 259 @Override 260 public void onProviderEnabled(String provider) { 261 } 262 263 @Override 264 public void onProviderDisabled(String provider) { 265 } 266 267 /** 268 * Calculates the twilight state for a specific location and time. 269 * 270 * @param location the location to use 271 * @param timeMillis the reference time to use 272 * @return the calculated {@link TwilightState}, or {@code null} if location is {@code null} 273 */ 274 private static TwilightState calculateTwilightState(Location location, long timeMillis) { 275 if (location == null) { 276 return null; 277 } 278 279 final CalendarAstronomer ca = new CalendarAstronomer( 280 location.getLongitude(), location.getLatitude()); 281 282 final Calendar noon = Calendar.getInstance(); 283 noon.setTimeInMillis(timeMillis); 284 noon.set(Calendar.HOUR_OF_DAY, 12); 285 noon.set(Calendar.MINUTE, 0); 286 noon.set(Calendar.SECOND, 0); 287 noon.set(Calendar.MILLISECOND, 0); 288 ca.setTime(noon.getTimeInMillis()); 289 290 long sunriseTimeMillis = ca.getSunRiseSet(true /* rise */); 291 long sunsetTimeMillis = ca.getSunRiseSet(false /* rise */); 292 293 if (sunsetTimeMillis < timeMillis) { 294 noon.add(Calendar.DATE, 1); 295 ca.setTime(noon.getTimeInMillis()); 296 sunriseTimeMillis = ca.getSunRiseSet(true /* rise */); 297 } else if (sunriseTimeMillis > timeMillis) { 298 noon.add(Calendar.DATE, -1); 299 ca.setTime(noon.getTimeInMillis()); 300 sunsetTimeMillis = ca.getSunRiseSet(false /* rise */); 301 } 302 303 return new TwilightState(sunriseTimeMillis, sunsetTimeMillis); 304 } 305 } 306