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