/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.twilight; import android.annotation.NonNull; import android.app.AlarmManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.icu.impl.CalendarAstronomer; import android.icu.util.Calendar; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.ArrayMap; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.server.SystemService; import java.util.Objects; /** * Figures out whether it's twilight time based on the user's location. *

* Used by the UI mode manager and other components to adjust night mode * effects based on sunrise and sunset. */ public final class TwilightService extends SystemService implements AlarmManager.OnAlarmListener, Handler.Callback, LocationListener { private static final String TAG = "TwilightService"; private static final boolean DEBUG = false; private static final int MSG_START_LISTENING = 1; private static final int MSG_STOP_LISTENING = 2; @GuardedBy("mListeners") private final ArrayMap mListeners = new ArrayMap<>(); private final Handler mHandler; private AlarmManager mAlarmManager; private LocationManager mLocationManager; private boolean mBootCompleted; private boolean mHasListeners; private BroadcastReceiver mTimeChangedReceiver; private Location mLastLocation; @GuardedBy("mListeners") private TwilightState mLastTwilightState; public TwilightService(Context context) { super(context); mHandler = new Handler(Looper.getMainLooper(), this); } @Override public void onStart() { publishLocalService(TwilightManager.class, new TwilightManager() { @Override public void registerListener(@NonNull TwilightListener listener, @NonNull Handler handler) { synchronized (mListeners) { final boolean wasEmpty = mListeners.isEmpty(); mListeners.put(listener, handler); if (wasEmpty && !mListeners.isEmpty()) { mHandler.sendEmptyMessage(MSG_START_LISTENING); } } } @Override public void unregisterListener(@NonNull TwilightListener listener) { synchronized (mListeners) { final boolean wasEmpty = mListeners.isEmpty(); mListeners.remove(listener); if (!wasEmpty && mListeners.isEmpty()) { mHandler.sendEmptyMessage(MSG_STOP_LISTENING); } } } @Override public TwilightState getLastTwilightState() { synchronized (mListeners) { return mLastTwilightState; } } }); } @Override public void onBootPhase(int phase) { if (phase == PHASE_BOOT_COMPLETED) { final Context c = getContext(); mAlarmManager = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE); mLocationManager = (LocationManager) c.getSystemService(Context.LOCATION_SERVICE); mBootCompleted = true; if (mHasListeners) { startListening(); } } } @Override public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_START_LISTENING: if (!mHasListeners) { mHasListeners = true; if (mBootCompleted) { startListening(); } } return true; case MSG_STOP_LISTENING: if (mHasListeners) { mHasListeners = false; if (mBootCompleted) { stopListening(); } } return true; } return false; } private void startListening() { Slog.d(TAG, "startListening"); // Start listening for location updates (default: low power, max 1h, min 10m). mLocationManager.requestLocationUpdates( null /* default */, this, Looper.getMainLooper()); // Request the device's location immediately if a previous location isn't available. if (mLocationManager.getLastLocation() == null) { if (mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { mLocationManager.requestSingleUpdate( LocationManager.NETWORK_PROVIDER, this, Looper.getMainLooper()); } else if (mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { mLocationManager.requestSingleUpdate( LocationManager.GPS_PROVIDER, this, Looper.getMainLooper()); } } // Update whenever the system clock is changed. if (mTimeChangedReceiver == null) { mTimeChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Slog.d(TAG, "onReceive: " + intent); updateTwilightState(); } }; final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED); intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); getContext().registerReceiver(mTimeChangedReceiver, intentFilter); } // Force an update now that we have listeners registered. updateTwilightState(); } private void stopListening() { Slog.d(TAG, "stopListening"); if (mTimeChangedReceiver != null) { getContext().unregisterReceiver(mTimeChangedReceiver); mTimeChangedReceiver = null; } if (mLastTwilightState != null) { mAlarmManager.cancel(this); } mLocationManager.removeUpdates(this); mLastLocation = null; } private void updateTwilightState() { // Calculate the twilight state based on the current time and location. final long currentTimeMillis = System.currentTimeMillis(); final Location location = mLastLocation != null ? mLastLocation : mLocationManager.getLastLocation(); final TwilightState state = calculateTwilightState(location, currentTimeMillis); if (DEBUG) { Slog.d(TAG, "updateTwilightState: " + state); } // Notify listeners if the state has changed. synchronized (mListeners) { if (!Objects.equals(mLastTwilightState, state)) { mLastTwilightState = state; for (int i = mListeners.size() - 1; i >= 0; --i) { final TwilightListener listener = mListeners.keyAt(i); final Handler handler = mListeners.valueAt(i); handler.post(new Runnable() { @Override public void run() { listener.onTwilightStateChanged(state); } }); } } } // Schedule an alarm to update the state at the next sunrise or sunset. if (state != null) { final long triggerAtMillis = state.isNight() ? state.sunriseTimeMillis() : state.sunsetTimeMillis(); mAlarmManager.setExact(AlarmManager.RTC, triggerAtMillis, TAG, this, mHandler); } } @Override public void onAlarm() { Slog.d(TAG, "onAlarm"); updateTwilightState(); } @Override public void onLocationChanged(Location location) { if (location != null) { Slog.d(TAG, "onLocationChanged:" + " provider=" + location.getProvider() + " accuracy=" + location.getAccuracy() + " time=" + location.getTime()); mLastLocation = location; updateTwilightState(); } } @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { } @Override public void onProviderDisabled(String provider) { } /** * Calculates the twilight state for a specific location and time. * * @param location the location to use * @param timeMillis the reference time to use * @return the calculated {@link TwilightState}, or {@code null} if location is {@code null} */ private static TwilightState calculateTwilightState(Location location, long timeMillis) { if (location == null) { return null; } final CalendarAstronomer ca = new CalendarAstronomer( location.getLongitude(), location.getLatitude()); final Calendar noon = Calendar.getInstance(); noon.setTimeInMillis(timeMillis); noon.set(Calendar.HOUR_OF_DAY, 12); noon.set(Calendar.MINUTE, 0); noon.set(Calendar.SECOND, 0); noon.set(Calendar.MILLISECOND, 0); ca.setTime(noon.getTimeInMillis()); long sunriseTimeMillis = ca.getSunRiseSet(true /* rise */); long sunsetTimeMillis = ca.getSunRiseSet(false /* rise */); if (sunsetTimeMillis < timeMillis) { noon.add(Calendar.DATE, 1); ca.setTime(noon.getTimeInMillis()); sunriseTimeMillis = ca.getSunRiseSet(true /* rise */); } else if (sunriseTimeMillis > timeMillis) { noon.add(Calendar.DATE, -1); ca.setTime(noon.getTimeInMillis()); sunsetTimeMillis = ca.getSunRiseSet(false /* rise */); } return new TwilightState(sunriseTimeMillis, sunsetTimeMillis); } }