TwilightService v2.0

- Switched to using CalendarAstronomer for more accurate sunrise/sunset
  times.
- Exposed sunrise/sunset times via TwilightState so that clients can
  track the current twilight period and perform their own
  interpolations.
- Adopted LocationRequest API for fused location updates:
  (low power, min 1h, max 10m).
- TwilightService is now only activated when a listener is registered,
  minimizing impact to system health on platforms / configurations
  where twilight state is not needed.

Bug: 28588307
Bug: 30190450
Bug: 30282370
Bug: 30650316
Change-Id: Ic5c94d8608e8bb3a3d895e623676a1468d4abdcd
diff --git a/services/core/java/com/android/server/TwilightCalculator.java b/services/core/java/com/android/server/TwilightCalculator.java
deleted file mode 100644
index 5839b16..0000000
--- a/services/core/java/com/android/server/TwilightCalculator.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2010 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;
-
-import android.text.format.DateUtils;
-
-/** @hide */
-public class TwilightCalculator {
-
-    /** Value of {@link #mState} if it is currently day */
-    public static final int DAY = 0;
-
-    /** Value of {@link #mState} if it is currently night */
-    public static final int NIGHT = 1;
-
-    private static final float DEGREES_TO_RADIANS = (float) (Math.PI / 180.0f);
-
-    // element for calculating solar transit.
-    private static final float J0 = 0.0009f;
-
-    // correction for civil twilight
-    private static final float ALTIDUTE_CORRECTION_CIVIL_TWILIGHT = -0.104719755f;
-
-    // coefficients for calculating Equation of Center.
-    private static final float C1 = 0.0334196f;
-    private static final float C2 = 0.000349066f;
-    private static final float C3 = 0.000005236f;
-
-    private static final float OBLIQUITY = 0.40927971f;
-
-    // Java time on Jan 1, 2000 12:00 UTC.
-    private static final long UTC_2000 = 946728000000L;
-
-    /**
-     * Time of sunset (civil twilight) in milliseconds or -1 in the case the day
-     * or night never ends.
-     */
-    public long mSunset;
-
-    /**
-     * Time of sunrise (civil twilight) in milliseconds or -1 in the case the
-     * day or night never ends.
-     */
-    public long mSunrise;
-
-    /** Current state */
-    public int mState;
-
-    /**
-     * calculates the civil twilight bases on time and geo-coordinates.
-     *
-     * @param time time in milliseconds.
-     * @param latiude latitude in degrees.
-     * @param longitude latitude in degrees.
-     */
-    public void calculateTwilight(long time, double latiude, double longitude) {
-        final float daysSince2000 = (float) (time - UTC_2000) / DateUtils.DAY_IN_MILLIS;
-
-        // mean anomaly
-        final float meanAnomaly = 6.240059968f + daysSince2000 * 0.01720197f;
-
-        // true anomaly
-        final double trueAnomaly = meanAnomaly + C1 * Math.sin(meanAnomaly) + C2
-                * Math.sin(2 * meanAnomaly) + C3 * Math.sin(3 * meanAnomaly);
-
-        // ecliptic longitude
-        final double solarLng = trueAnomaly + 1.796593063d + Math.PI;
-
-        // solar transit in days since 2000
-        final double arcLongitude = -longitude / 360;
-        float n = Math.round(daysSince2000 - J0 - arcLongitude);
-        double solarTransitJ2000 = n + J0 + arcLongitude + 0.0053d * Math.sin(meanAnomaly)
-                + -0.0069d * Math.sin(2 * solarLng);
-
-        // declination of sun
-        double solarDec = Math.asin(Math.sin(solarLng) * Math.sin(OBLIQUITY));
-
-        final double latRad = latiude * DEGREES_TO_RADIANS;
-
-        double cosHourAngle = (Math.sin(ALTIDUTE_CORRECTION_CIVIL_TWILIGHT) - Math.sin(latRad)
-                * Math.sin(solarDec)) / (Math.cos(latRad) * Math.cos(solarDec));
-        // The day or night never ends for the given date and location, if this value is out of
-        // range.
-        if (cosHourAngle >= 1) {
-            mState = NIGHT;
-            mSunset = -1;
-            mSunrise = -1;
-            return;
-        } else if (cosHourAngle <= -1) {
-            mState = DAY;
-            mSunset = -1;
-            mSunrise = -1;
-            return;
-        }
-
-        float hourAngle = (float) (Math.acos(cosHourAngle) / (2 * Math.PI));
-
-        mSunset = Math.round((solarTransitJ2000 + hourAngle) * DateUtils.DAY_IN_MILLIS) + UTC_2000;
-        mSunrise = Math.round((solarTransitJ2000 - hourAngle) * DateUtils.DAY_IN_MILLIS) + UTC_2000;
-
-        if (mSunrise < time && mSunset > time) {
-            mState = DAY;
-        } else {
-            mState = NIGHT;
-        }
-    }
-
-}
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 6f713cd..bb5f62b 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityManagerNative;
@@ -155,8 +156,13 @@
 
     private final TwilightListener mTwilightListener = new TwilightListener() {
         @Override
-        public void onTwilightStateChanged() {
-            updateTwilight();
+        public void onTwilightStateChanged(@Nullable TwilightState state) {
+            synchronized (mLock) {
+                if (mNightMode == UiModeManager.MODE_NIGHT_AUTO) {
+                    updateComputedNightModeLocked();
+                    updateLocked(0, 0);
+                }
+            }
         }
     };
 
@@ -344,8 +350,8 @@
                     pw.print(" mSystemReady="); pw.println(mSystemReady);
             if (mTwilightManager != null) {
                 // We may not have a TwilightManager.
-                pw.print("  mTwilightService.getCurrentState()=");
-                pw.println(mTwilightManager.getCurrentState());
+                pw.print("  mTwilightService.getLastTwilightState()=");
+                pw.println(mTwilightManager.getLastTwilightState());
             }
         }
     }
@@ -355,9 +361,6 @@
         if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
             synchronized (mLock) {
                 mTwilightManager = getLocalService(TwilightManager.class);
-                if (mTwilightManager != null) {
-                    mTwilightManager.registerListener(mTwilightListener, mHandler);
-                }
                 mSystemReady = true;
                 mCarModeEnabled = mDockState == Intent.EXTRA_DOCK_STATE_CAR;
                 updateComputedNightModeLocked();
@@ -411,10 +414,16 @@
         }
 
         if (mNightMode == UiModeManager.MODE_NIGHT_AUTO) {
+            if (mTwilightManager != null) {
+                mTwilightManager.registerListener(mTwilightListener, mHandler);
+            }
             updateComputedNightModeLocked();
             uiMode |= mComputedNightMode ? Configuration.UI_MODE_NIGHT_YES
                     : Configuration.UI_MODE_NIGHT_NO;
         } else {
+            if (mTwilightManager != null) {
+                mTwilightManager.unregisterListener(mTwilightListener);
+            }
             uiMode |= mNightMode << 4;
         }
 
@@ -668,18 +677,9 @@
         }
     }
 
-    void updateTwilight() {
-        synchronized (mLock) {
-            if (mNightMode == UiModeManager.MODE_NIGHT_AUTO) {
-                updateComputedNightModeLocked();
-                updateLocked(0, 0);
-            }
-        }
-    }
-
     private void updateComputedNightModeLocked() {
         if (mTwilightManager != null) {
-            TwilightState state = mTwilightManager.getCurrentState();
+            TwilightState state = mTwilightManager.getLastTwilightState();
             if (state != null) {
                 mComputedNightMode = state.isNight();
             }
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index c16dac2..15ae846 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -22,6 +22,7 @@
 import com.android.server.twilight.TwilightManager;
 import com.android.server.twilight.TwilightState;
 
+import android.annotation.Nullable;
 import android.hardware.Sensor;
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
@@ -268,7 +269,7 @@
         pw.println();
         pw.println("Automatic Brightness Controller State:");
         pw.println("  mLightSensor=" + mLightSensor);
-        pw.println("  mTwilight.getCurrentState()=" + mTwilight.getCurrentState());
+        pw.println("  mTwilight.getLastTwilightState()=" + mTwilight.getLastTwilightState());
         pw.println("  mLightSensorEnabled=" + mLightSensorEnabled);
         pw.println("  mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime));
         pw.println("  mAmbientLux=" + mAmbientLux);
@@ -495,12 +496,14 @@
         }
 
         if (mUseTwilight) {
-            TwilightState state = mTwilight.getCurrentState();
+            TwilightState state = mTwilight.getLastTwilightState();
             if (state != null && state.isNight()) {
-                final long now = System.currentTimeMillis();
-                gamma *= 1 + state.getAmount() * TWILIGHT_ADJUSTMENT_MAX_GAMMA;
+                final long duration = state.sunriseTimeMillis() - state.sunsetTimeMillis();
+                final long progress = System.currentTimeMillis() - state.sunsetTimeMillis();
+                final float amount = (float) Math.pow(2.0 * progress / duration - 1.0, 2.0);
+                gamma *= 1 + amount * TWILIGHT_ADJUSTMENT_MAX_GAMMA;
                 if (DEBUG) {
-                    Slog.d(TAG, "updateAutoBrightness: twilight amount=" + state.getAmount());
+                    Slog.d(TAG, "updateAutoBrightness: twilight amount=" + amount);
                 }
             }
         }
@@ -621,7 +624,7 @@
 
     private final TwilightListener mTwilightListener = new TwilightListener() {
         @Override
-        public void onTwilightStateChanged() {
+        public void onTwilightStateChanged(@Nullable TwilightState state) {
             updateAutoBrightness(true /*sendUpdate*/);
         }
     };
diff --git a/services/core/java/com/android/server/display/NightDisplayService.java b/services/core/java/com/android/server/display/NightDisplayService.java
index 39498a6..d9af7cb 100644
--- a/services/core/java/com/android/server/display/NightDisplayService.java
+++ b/services/core/java/com/android/server/display/NightDisplayService.java
@@ -20,6 +20,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.TypeEvaluator;
 import android.animation.ValueAnimator;
+import android.annotation.Nullable;
 import android.app.AlarmManager;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -217,12 +218,12 @@
         if (mIsActivated == null || mIsActivated != activated) {
             Slog.i(TAG, activated ? "Turning on night display" : "Turning off night display");
 
-            mIsActivated = activated;
-
             if (mAutoMode != null) {
                 mAutoMode.onActivated(activated);
             }
 
+            mIsActivated = activated;
+
             // Cancel the old animator if still running.
             if (mColorMatrixAnimator != null) {
                 mColorMatrixAnimator.cancel();
@@ -395,7 +396,9 @@
 
         @Override
         public void onActivated(boolean activated) {
-            mLastActivatedTime = Calendar.getInstance();
+            if (mIsActivated != null) {
+                mLastActivatedTime = Calendar.getInstance();
+            }
             updateNextAlarm();
         }
 
@@ -424,22 +427,30 @@
 
         private final TwilightManager mTwilightManager;
 
-        private boolean mIsNight;
+        private Calendar mLastActivatedTime;
 
         public TwilightAutoMode() {
             mTwilightManager = getLocalService(TwilightManager.class);
         }
 
-        private void updateActivated() {
-            final TwilightState state = mTwilightManager.getCurrentState();
+        private void updateActivated(TwilightState state) {
             final boolean isNight = state != null && state.isNight();
-            if (mIsNight != isNight) {
-                mIsNight = isNight;
-
-                if (mIsActivated == null || mIsActivated != isNight) {
-                    mController.setActivated(isNight);
+            boolean setActivated = mIsActivated == null || mIsActivated != isNight;
+            if (setActivated && state != null && mLastActivatedTime != null) {
+                final Calendar sunrise = state.sunrise();
+                final Calendar sunset = state.sunset();
+                if (sunrise.before(sunset)) {
+                    setActivated = mLastActivatedTime.before(sunrise)
+                            || mLastActivatedTime.after(sunset);
+                } else {
+                    setActivated = mLastActivatedTime.before(sunset)
+                            || mLastActivatedTime.after(sunrise);
                 }
             }
+
+            if (setActivated) {
+                mController.setActivated(isNight);
+            }
         }
 
         @Override
@@ -447,18 +458,26 @@
             mTwilightManager.registerListener(this, mHandler);
 
             // Force an update to initialize state.
-            updateActivated();
+            updateActivated(mTwilightManager.getLastTwilightState());
         }
 
         @Override
         public void onStop() {
             mTwilightManager.unregisterListener(this);
+            mLastActivatedTime = null;
         }
 
         @Override
-        public void onTwilightStateChanged() {
+        public void onActivated(boolean activated) {
+            if (mIsActivated != null) {
+                mLastActivatedTime = Calendar.getInstance();
+            }
+        }
+
+        @Override
+        public void onTwilightStateChanged(@Nullable TwilightState state) {
             if (DEBUG) Slog.d(TAG, "onTwilightStateChanged");
-            updateActivated();
+            updateActivated(state);
         }
     }
 
diff --git a/services/core/java/com/android/server/twilight/TwilightListener.java b/services/core/java/com/android/server/twilight/TwilightListener.java
index 29ead44..58dcef6 100644
--- a/services/core/java/com/android/server/twilight/TwilightListener.java
+++ b/services/core/java/com/android/server/twilight/TwilightListener.java
@@ -16,6 +16,14 @@
 
 package com.android.server.twilight;
 
+import android.annotation.Nullable;
+
+/**
+ * Callback for when the twilight state has changed.
+ */
 public interface TwilightListener {
-    void onTwilightStateChanged();
+    /**
+     * Called when the twilight state has changed.
+     */
+    void onTwilightStateChanged(@Nullable TwilightState state);
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/twilight/TwilightManager.java b/services/core/java/com/android/server/twilight/TwilightManager.java
index 56137a4..5ef9417 100644
--- a/services/core/java/com/android/server/twilight/TwilightManager.java
+++ b/services/core/java/com/android/server/twilight/TwilightManager.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * 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.
@@ -16,10 +16,30 @@
 
 package com.android.server.twilight;
 
+import android.annotation.NonNull;
 import android.os.Handler;
 
+/**
+ * This class provides sunrise/sunset information based on the device's current location.
+ */
 public interface TwilightManager {
-    void registerListener(TwilightListener listener, Handler handler);
-    void unregisterListener(TwilightListener listener);
-    TwilightState getCurrentState();
+    /**
+     * Register a listener to be notified whenever the twilight state changes.
+     *
+     * @param listener the {@link TwilightListener} to be notified
+     * @param handler the {@link Handler} to use to notify the listener
+     */
+    void registerListener(@NonNull TwilightListener listener, @NonNull Handler handler);
+
+    /**
+     * Unregisters a previously registered listener.
+     *
+     * @param listener the {@link TwilightListener} to be unregistered
+     */
+    void unregisterListener(@NonNull TwilightListener listener);
+
+    /**
+     * Returns the last {@link TwilightState}, or {@code null} if not available.
+     */
+    TwilightState getLastTwilightState();
 }
diff --git a/services/core/java/com/android/server/twilight/TwilightService.java b/services/core/java/com/android/server/twilight/TwilightService.java
index ee7a4a0..acd6587 100644
--- a/services/core/java/com/android/server/twilight/TwilightService.java
+++ b/services/core/java/com/android/server/twilight/TwilightService.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012 The Android Open Source Project
+ * 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.
@@ -16,31 +16,27 @@
 
 package com.android.server.twilight;
 
+import android.annotation.NonNull;
 import android.app.AlarmManager;
-import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.location.Criteria;
+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.os.SystemClock;
-import android.text.format.DateUtils;
-import android.text.format.Time;
+import android.util.ArrayMap;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.SystemService;
-import com.android.server.TwilightCalculator;
 
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
 import java.util.Objects;
 
 /**
@@ -49,476 +45,261 @@
  * 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 {
+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 String ACTION_UPDATE_TWILIGHT_STATE =
-            "com.android.server.action.UPDATE_TWILIGHT_STATE";
+    private static final int MSG_START_LISTENING = 1;
+    private static final int MSG_STOP_LISTENING = 2;
 
-    /**
-     * The amount of time after or before sunrise over which to start adjusting twilight affected
-     * things. We want the change to happen gradually so that it is below the threshold of
-     * perceptibility and so that the adjustment has and so that the adjustment has
-     * maximum effect well after dusk.
-     */
-    private static final long TWILIGHT_ADJUSTMENT_TIME = DateUtils.HOUR_IN_MILLIS * 2;
+    @GuardedBy("mListeners")
+    private final ArrayMap<TwilightListener, Handler> mListeners = new ArrayMap<>();
 
-    private final Object mLock = new Object();
-
-    @GuardedBy("mLock")
-    private final List<TwilightListenerRecord> mListeners = new ArrayList<>();
+    private final Handler mHandler;
 
     private AlarmManager mAlarmManager;
     private LocationManager mLocationManager;
-    private LocationHandler mLocationHandler;
 
-    @GuardedBy("mLock")
-    private TwilightState mTwilightState;
+    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() {
-        mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
-        mLocationManager = (LocationManager) getContext().getSystemService(
-                Context.LOCATION_SERVICE);
-        mLocationHandler = new LocationHandler();
+        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);
 
-        IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        filter.addAction(Intent.ACTION_TIME_CHANGED);
-        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
-        filter.addAction(Intent.ACTION_USER_SWITCHED);
-        getContext().registerReceiver(mReceiver, filter);
+                    if (wasEmpty && !mListeners.isEmpty()) {
+                        mHandler.sendEmptyMessage(MSG_START_LISTENING);
+                    }
+                }
+            }
 
-        publishLocalService(TwilightManager.class, mService);
+            @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) {
-            // Initialize the current twilight state.
-            mLocationHandler.requestTwilightUpdate();
-        }
-    }
+            final Context c = getContext();
+            mAlarmManager = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE);
+            mLocationManager = (LocationManager) c.getSystemService(Context.LOCATION_SERVICE);
 
-    private void setTwilightState(TwilightState state) {
-        synchronized (mLock) {
-            if (!Objects.equals(mTwilightState, state)) {
-                if (DEBUG) {
-                    Slog.d(TAG, "Twilight state changed: " + state);
-                }
-
-                mTwilightState = state;
-
-                for (TwilightListenerRecord mListener : mListeners) {
-                    mListener.postUpdate();
-                }
+            mBootCompleted = true;
+            if (mHasListeners) {
+                startListening();
             }
         }
     }
 
-    private static class TwilightListenerRecord implements Runnable {
-
-        private final TwilightListener mListener;
-        private final Handler mHandler;
-
-        public TwilightListenerRecord(TwilightListener listener, Handler handler) {
-            mListener = listener;
-            mHandler = handler;
+    @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;
         }
-
-        public void postUpdate() {
-            mHandler.post(this);
-        }
-
-        @Override
-        public void run() {
-            mListener.onTwilightStateChanged();
-        }
+        return false;
     }
 
-    private final TwilightManager mService = new TwilightManager() {
-        @Override
-        public TwilightState getCurrentState() {
-            synchronized (mLock) {
-                return mTwilightState;
+    private void startListening() {
+        if (DEBUG) 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());
             }
         }
 
-        @Override
-        public void registerListener(TwilightListener listener, Handler handler) {
-            synchronized (mLock) {
-                mListeners.add(new TwilightListenerRecord(listener, handler));
-
-                if (mListeners.size() == 1) {
-                    mLocationHandler.enableLocationUpdates();
-                }
-            }
-        }
-
-        @Override
-        public void unregisterListener(TwilightListener listener) {
-            synchronized (mLock) {
-                for (int i = 0; i < mListeners.size(); i++) {
-                    if (mListeners.get(i).mListener == listener) {
-                        mListeners.remove(i);
-                    }
-                }
-
-                if (mListeners.size() == 0) {
-                    mLocationHandler.disableLocationUpdates();
-                }
-            }
-        }
-    };
-
-    // The user has moved if the accuracy circles of the two locations don't overlap.
-    private static boolean hasMoved(Location from, Location to) {
-        if (to == null) {
-            return false;
-        }
-
-        if (from == null) {
-            return true;
-        }
-
-        // if new location is older than the current one, the device hasn't moved.
-        if (to.getElapsedRealtimeNanos() < from.getElapsedRealtimeNanos()) {
-            return false;
-        }
-
-        // Get the distance between the two points.
-        float distance = from.distanceTo(to);
-
-        // Get the total accuracy radius for both locations.
-        float totalAccuracy = from.getAccuracy() + to.getAccuracy();
-
-        // If the distance is greater than the combined accuracy of the two
-        // points then they can't overlap and hence the user has moved.
-        return distance >= totalAccuracy;
-    }
-
-    private final class LocationHandler extends Handler {
-
-        private static final int MSG_ENABLE_LOCATION_UPDATES = 1;
-        private static final int MSG_GET_NEW_LOCATION_UPDATE = 2;
-        private static final int MSG_PROCESS_NEW_LOCATION = 3;
-        private static final int MSG_DO_TWILIGHT_UPDATE = 4;
-        private static final int MSG_DISABLE_LOCATION_UPDATES = 5;
-
-        private static final long LOCATION_UPDATE_MS = 24 * DateUtils.HOUR_IN_MILLIS;
-        private static final long MIN_LOCATION_UPDATE_MS = 30 * DateUtils.MINUTE_IN_MILLIS;
-        private static final float LOCATION_UPDATE_DISTANCE_METER = 1000 * 20;
-        private static final long LOCATION_UPDATE_ENABLE_INTERVAL_MIN = 5000;
-        private static final long LOCATION_UPDATE_ENABLE_INTERVAL_MAX =
-                15 * DateUtils.MINUTE_IN_MILLIS;
-        private static final double FACTOR_GMT_OFFSET_LONGITUDE =
-                1000.0 * 360.0 / DateUtils.DAY_IN_MILLIS;
-
-        private final TwilightCalculator mTwilightCalculator = new TwilightCalculator();
-
-        private boolean mPassiveListenerEnabled;
-        private boolean mNetworkListenerEnabled;
-        private boolean mDidFirstInit;
-        private long mLastNetworkRegisterTime = -MIN_LOCATION_UPDATE_MS;
-        private long mLastUpdateInterval;
-        private Location mLocation;
-
-        public void processNewLocation(Location location) {
-            Message msg = obtainMessage(MSG_PROCESS_NEW_LOCATION, location);
-            sendMessage(msg);
-        }
-
-        public void enableLocationUpdates() {
-            sendEmptyMessage(MSG_ENABLE_LOCATION_UPDATES);
-        }
-
-        public void disableLocationUpdates() {
-            sendEmptyMessage(MSG_DISABLE_LOCATION_UPDATES);
-        }
-
-        public void requestLocationUpdate() {
-            sendEmptyMessage(MSG_GET_NEW_LOCATION_UPDATE);
-        }
-
-        public void requestTwilightUpdate() {
-            sendEmptyMessage(MSG_DO_TWILIGHT_UPDATE);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_PROCESS_NEW_LOCATION: {
-                    final Location location = (Location) msg.obj;
-                    final boolean hasMoved = hasMoved(mLocation, location);
-                    final boolean hasBetterAccuracy = mLocation == null
-                            || location.getAccuracy() < mLocation.getAccuracy();
-                    if (DEBUG) {
-                        Slog.d(TAG, "Processing new location: " + location
-                                + ", hasMoved=" + hasMoved
-                                + ", hasBetterAccuracy=" + hasBetterAccuracy);
-                    }
-                    if (hasMoved || hasBetterAccuracy) {
-                        setLocation(location);
-                    }
-                    break;
-                }
-
-                case MSG_GET_NEW_LOCATION_UPDATE:
-                    if (!mNetworkListenerEnabled) {
-                        // Don't do anything -- we are still trying to get a
-                        // location.
-                        return;
-                    }
-                    if ((mLastNetworkRegisterTime + MIN_LOCATION_UPDATE_MS) >=
-                            SystemClock.elapsedRealtime()) {
-                        // Don't do anything -- it hasn't been long enough
-                        // since we last requested an update.
-                        return;
-                    }
-
-                    // Unregister the current location monitor, so we can
-                    // register a new one for it to get an immediate update.
-                    mNetworkListenerEnabled = false;
-                    mLocationManager.removeUpdates(mEmptyLocationListener);
-
-                    // Fall through to re-register listener.
-                case MSG_ENABLE_LOCATION_UPDATES:
-                    // enable network provider to receive at least location updates for a given
-                    // distance.
-                    boolean networkLocationEnabled;
-                    try {
-                        networkLocationEnabled = mLocationManager.isProviderEnabled(
-                                LocationManager.NETWORK_PROVIDER);
-                    } catch (Exception e) {
-                        // we may get IllegalArgumentException if network location provider
-                        // does not exist or is not yet installed.
-                        networkLocationEnabled = false;
-                    }
-                    if (!mNetworkListenerEnabled && networkLocationEnabled) {
-                        mNetworkListenerEnabled = true;
-                        mLastNetworkRegisterTime = SystemClock.elapsedRealtime();
-                        mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
-                                LOCATION_UPDATE_MS, 0, mEmptyLocationListener);
-
-                        if (!mDidFirstInit) {
-                            mDidFirstInit = true;
-                            if (mLocation == null) {
-                                retrieveLocation();
-                            }
-                        }
-                    }
-
-                    // enable passive provider to receive updates from location fixes (gps
-                    // and network).
-                    boolean passiveLocationEnabled;
-                    try {
-                        passiveLocationEnabled = mLocationManager.isProviderEnabled(
-                                LocationManager.PASSIVE_PROVIDER);
-                    } catch (Exception e) {
-                        // we may get IllegalArgumentException if passive location provider
-                        // does not exist or is not yet installed.
-                        passiveLocationEnabled = false;
-                    }
-
-                    if (!mPassiveListenerEnabled && passiveLocationEnabled) {
-                        mPassiveListenerEnabled = true;
-                        mLocationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER,
-                                0, LOCATION_UPDATE_DISTANCE_METER, mLocationListener);
-                    }
-
-                    if (!(mNetworkListenerEnabled && mPassiveListenerEnabled)) {
-                        mLastUpdateInterval *= 1.5;
-                        if (mLastUpdateInterval == 0) {
-                            mLastUpdateInterval = LOCATION_UPDATE_ENABLE_INTERVAL_MIN;
-                        } else if (mLastUpdateInterval > LOCATION_UPDATE_ENABLE_INTERVAL_MAX) {
-                            mLastUpdateInterval = LOCATION_UPDATE_ENABLE_INTERVAL_MAX;
-                        }
-                        sendEmptyMessageDelayed(MSG_ENABLE_LOCATION_UPDATES, mLastUpdateInterval);
-                    }
-                    break;
-
-                case MSG_DISABLE_LOCATION_UPDATES:
-                    mLocationManager.removeUpdates(mLocationListener);
-                    removeMessages(MSG_ENABLE_LOCATION_UPDATES);
-                    break;
-
-                case MSG_DO_TWILIGHT_UPDATE:
+        // Update whenever the system clock is changed.
+        if (mTimeChangedReceiver == null) {
+            mTimeChangedReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    if (DEBUG) Slog.d(TAG, "onReceive: " + intent);
                     updateTwilightState();
-                    break;
+                }
+            };
+
+            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() {
+        if (DEBUG) 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);
+                        }
+                    });
+                }
             }
         }
 
-        private void retrieveLocation() {
-            Location location = null;
-            final Iterator<String> providers =
-                    mLocationManager.getProviders(new Criteria(), true).iterator();
-            while (providers.hasNext()) {
-                final Location lastKnownLocation =
-                        mLocationManager.getLastKnownLocation(providers.next());
-                // pick the most recent location
-                if (location == null || (lastKnownLocation != null &&
-                        location.getElapsedRealtimeNanos() <
-                                lastKnownLocation.getElapsedRealtimeNanos())) {
-                    location = lastKnownLocation;
-                }
-            }
-
-            // In the case there is no location available (e.g. GPS fix or network location
-            // is not available yet), the longitude of the location is estimated using the
-            // timezone, latitude and accuracy are set to get a good average.
-            if (location == null) {
-                Time currentTime = new Time();
-                currentTime.set(System.currentTimeMillis());
-                double lngOffset = FACTOR_GMT_OFFSET_LONGITUDE *
-                        (currentTime.gmtoff - (currentTime.isDst > 0 ? 3600 : 0));
-                location = new Location("fake");
-                location.setLongitude(lngOffset);
-                location.setLatitude(0);
-                location.setAccuracy(417000.0f);
-                location.setTime(System.currentTimeMillis());
-                location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
-
-                if (DEBUG) {
-                    Slog.d(TAG, "Estimated location from timezone: " + location);
-                }
-            }
-
-            setLocation(location);
-        }
-
-        private void setLocation(Location location) {
-            mLocation = location;
-            updateTwilightState();
-        }
-
-        private void updateTwilightState() {
-            if (mLocation == null) {
-                setTwilightState(null);
-                return;
-            }
-
-            final long now = System.currentTimeMillis();
-
-            // calculate today's twilight
-            mTwilightCalculator.calculateTwilight(now,
-                    mLocation.getLatitude(), mLocation.getLongitude());
-            final boolean isNight = (mTwilightCalculator.mState == TwilightCalculator.NIGHT);
-            final long todaySunrise = mTwilightCalculator.mSunrise;
-            final long todaySunset = mTwilightCalculator.mSunset;
-
-            // calculate tomorrow's twilight
-            mTwilightCalculator.calculateTwilight(now + DateUtils.DAY_IN_MILLIS,
-                    mLocation.getLatitude(), mLocation.getLongitude());
-            final long tomorrowSunrise = mTwilightCalculator.mSunrise;
-
-            float amount = 0;
-            if (isNight) {
-                if (todaySunrise == -1 || todaySunset == -1) {
-                    amount = 1;
-                } else if (now > todaySunset) {
-                    amount = Math.min(1, (now - todaySunset) / (float) TWILIGHT_ADJUSTMENT_TIME);
-                } else {
-                    amount = Math.max(0, 1
-                            - (todaySunrise - now) / (float) TWILIGHT_ADJUSTMENT_TIME);
-                }
-            }
-            // set twilight state
-            TwilightState state = new TwilightState(isNight, amount);
-            if (DEBUG) {
-                Slog.d(TAG, "Updating twilight state: " + state);
-            }
-            setTwilightState(state);
-
-            // schedule next update
-            long nextUpdate = 0;
-            if (todaySunrise == -1 || todaySunset == -1) {
-                // In the case the day or night never ends the update is scheduled 12 hours later.
-                nextUpdate = now + 12 * DateUtils.HOUR_IN_MILLIS;
-            } else {
-                // add some extra time to be on the safe side.
-                nextUpdate += DateUtils.MINUTE_IN_MILLIS;
-
-                if (amount == 1 || amount == 0) {
-                    if (now > todaySunset) {
-                        nextUpdate += tomorrowSunrise;
-                    } else if (now > todaySunrise) {
-                        nextUpdate += todaySunset;
-                    } else {
-                        nextUpdate += todaySunrise;
-                    }
-                } else {
-                    // This is the update rate while transitioning.
-                    // Leave at 10 min for now (one from above).
-                    nextUpdate += 9 * DateUtils.MINUTE_IN_MILLIS;
-                }
-            }
-
-            if (DEBUG) {
-                Slog.d(TAG, "Next update in " + (nextUpdate - now) + " ms");
-            }
-
-            final PendingIntent pendingIntent = PendingIntent.getBroadcast(
-                    getContext(), 0, new Intent(ACTION_UPDATE_TWILIGHT_STATE), 0);
-            mAlarmManager.cancel(pendingIntent);
-            mAlarmManager.setExact(AlarmManager.RTC, nextUpdate, pendingIntent);
+        // 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);
         }
     }
 
-    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())
-                    && !intent.getBooleanExtra("state", false)) {
-                // Airplane mode is now off!
-                mLocationHandler.requestLocationUpdate();
-                return;
-            }
-            // Time zone has changed or alarm expired.
-            mLocationHandler.requestTwilightUpdate();
-        }
-    };
+    @Override
+    public void onAlarm() {
+        if (DEBUG) Slog.d(TAG, "onAlarm");
+        updateTwilightState();
+    }
 
-    // A LocationListener to initialize the network location provider. The location updates
-    // are handled through the passive location provider.
-    private final LocationListener mEmptyLocationListener = new LocationListener() {
-        @Override
-        public void onLocationChanged(Location location) {
+    @Override
+    public void onLocationChanged(Location location) {
+        if (DEBUG) Slog.d(TAG, "onLocationChanged: " + location);
+        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;
         }
 
-        @Override
-        public void onStatusChanged(String provider, int status, Bundle extras) {
+        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 */);
         }
 
-        @Override
-        public void onProviderEnabled(String provider) {
-        }
-
-        @Override
-        public void onProviderDisabled(String provider) {
-        }
-    };
-
-    private final LocationListener mLocationListener = new LocationListener() {
-        @Override
-        public void onLocationChanged(Location location) {
-            mLocationHandler.processNewLocation(location);
-        }
-
-        @Override
-        public void onStatusChanged(String provider, int status, Bundle extras) {
-        }
-
-        @Override
-        public void onProviderEnabled(String provider) {
-        }
-
-        @Override
-        public void onProviderDisabled(String provider) {
-        }
-    };
+        return new TwilightState(sunriseTimeMillis, sunsetTimeMillis);
+    }
 }
diff --git a/services/core/java/com/android/server/twilight/TwilightState.java b/services/core/java/com/android/server/twilight/TwilightState.java
index dec053b..a12965d 100644
--- a/services/core/java/com/android/server/twilight/TwilightState.java
+++ b/services/core/java/com/android/server/twilight/TwilightState.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013 The Android Open Source Project
+ * 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.
@@ -16,59 +16,89 @@
 
 package com.android.server.twilight;
 
-import java.text.DateFormat;
-import java.util.Date;
+import android.text.format.DateFormat;
+
+import java.util.Calendar;
 
 /**
- * Describes whether it is day or night.
- * This object is immutable.
+ * The twilight state, consisting of the sunrise and sunset times (in millis) for the current
+ * period.
+ * <p/>
+ * Note: This object is immutable.
  */
-public class TwilightState {
+public final class TwilightState {
 
-    private final boolean mIsNight;
-    private final float mAmount;
+    private final long mSunriseTimeMillis;
+    private final long mSunsetTimeMillis;
 
-    TwilightState(boolean isNight, float amount) {
-        mIsNight = isNight;
-        mAmount = amount;
+    TwilightState(long sunriseTimeMillis, long sunsetTimeMillis) {
+        mSunriseTimeMillis = sunriseTimeMillis;
+        mSunsetTimeMillis = sunsetTimeMillis;
     }
 
     /**
-     * Returns true if it is currently night time.
+     * Returns the time (in UTC milliseconds from epoch) of the upcoming or previous sunrise if
+     * it's night or day respectively.
+     */
+    public long sunriseTimeMillis() {
+        return mSunriseTimeMillis;
+    }
+
+    /**
+     * Returns a new {@link Calendar} instance initialized to {@link #sunriseTimeMillis()}.
+     */
+    public Calendar sunrise() {
+        final Calendar sunrise = Calendar.getInstance();
+        sunrise.setTimeInMillis(mSunriseTimeMillis);
+        return sunrise;
+    }
+
+    /**
+     * Returns the time (in UTC milliseconds from epoch) of the upcoming or previous sunset if
+     * it's day or night respectively.
+     */
+    public long sunsetTimeMillis() {
+        return mSunsetTimeMillis;
+    }
+
+    /**
+     * Returns a new {@link Calendar} instance initialized to {@link #sunsetTimeMillis()}.
+     */
+    public Calendar sunset() {
+        final Calendar sunset = Calendar.getInstance();
+        sunset.setTimeInMillis(mSunsetTimeMillis);
+        return sunset;
+    }
+
+    /**
+     * Returns {@code true} if it is currently night time.
      */
     public boolean isNight() {
-        return mIsNight;
-    }
-
-    /**
-     * For twilight affects that change gradually over time, this is the amount they
-     * should currently be in effect.
-     */
-    public float getAmount() {
-        return mAmount;
+        final long now = System.currentTimeMillis();
+        return now >= mSunsetTimeMillis && now < mSunriseTimeMillis;
     }
 
     @Override
     public boolean equals(Object o) {
-        return o instanceof TwilightState && equals((TwilightState)o);
+        return o instanceof TwilightState && equals((TwilightState) o);
     }
 
     public boolean equals(TwilightState other) {
         return other != null
-                && mIsNight == other.mIsNight
-                && mAmount == other.mAmount;
+                && mSunriseTimeMillis == other.mSunriseTimeMillis
+                && mSunsetTimeMillis == other.mSunsetTimeMillis;
     }
 
     @Override
     public int hashCode() {
-        return 0; // don't care
+        return Long.hashCode(mSunriseTimeMillis) ^ Long.hashCode(mSunsetTimeMillis);
     }
 
     @Override
     public String toString() {
-        DateFormat f = DateFormat.getDateTimeInstance();
-        return "{TwilightState: isNight=" + mIsNight
-                + ", mAmount=" + mAmount
-                + "}";
+        return "TwilightState {"
+                + " sunrise=" + DateFormat.format("MM-dd HH:mm", mSunriseTimeMillis)
+                + " sunset="+ DateFormat.format("MM-dd HH:mm", mSunsetTimeMillis)
+                + " }";
     }
 }