blob: db7df25da3dea847d411c0c732cf6f6a69e5f726 [file] [log] [blame]
/*
* 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.
* <p>
* 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<TwilightListener, Handler> 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);
}
}