From f13838a099eb91c504249ba7cce195ffc6bf183a Mon Sep 17 00:00:00 2001 From: Arne Coucheron Date: Sun, 27 May 2018 21:22:30 +0200 Subject: [PATCH] Revert "lineage-sdk: Switch back to AOSP TwilightService" * Causing issues on several devices, with that the GPS gets stuck in ON state and can't be turned off * No timezone fallback This reverts commit cac65b89721188e9873b0572b32b13bec81f0d76. Change-Id: Ie9b0249a1e6f94c374e90f89f49d506b1ec6c50d --- .../display/ColorTemperatureController.java | 85 ++- .../internal/display/LiveDisplayFeature.java | 3 +- .../internal/display/LiveDisplayService.java | 23 +- .../internal/display/TwilightCalculator.java | 123 ++++ .../internal/display/TwilightTracker.java | 562 ++++++++++++++++++ 5 files changed, 734 insertions(+), 62 deletions(-) create mode 100644 lineage/lib/main/java/org/lineageos/platform/internal/display/TwilightCalculator.java create mode 100644 lineage/lib/main/java/org/lineageos/platform/internal/display/TwilightTracker.java diff --git a/lineage/lib/main/java/org/lineageos/platform/internal/display/ColorTemperatureController.java b/lineage/lib/main/java/org/lineageos/platform/internal/display/ColorTemperatureController.java index 67853a47..7dbd2874 100644 --- a/lineage/lib/main/java/org/lineageos/platform/internal/display/ColorTemperatureController.java +++ b/lineage/lib/main/java/org/lineageos/platform/internal/display/ColorTemperatureController.java @@ -1,6 +1,5 @@ /* * Copyright (C) 2016 The CyanogenMod Project - * Copyright (C) 2018 The LineageOS Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +26,7 @@ import android.util.Range; import android.util.Slog; import android.view.animation.LinearInterpolator; -import com.android.server.twilight.TwilightState; +import org.lineageos.platform.internal.display.TwilightTracker.TwilightState; import java.io.PrintWriter; import java.util.BitSet; @@ -192,15 +191,7 @@ public class ColorTemperatureController extends LiveDisplayFeature { } else if (mode == MODE_NIGHT) { temperature = mNightTemperature; } else if (mode == MODE_AUTO) { - final int twilightTemp = getTwilightK(); - if (twilightTemp >= 0) { - temperature = twilightTemp; - } else { - if (DEBUG) { - Slog.d(TAG, "updateColorTemperature: getTwilightK returned < 0; " - + "maintaining existing temperature"); - } - } + temperature = getTwilightK(); } if (DEBUG) { @@ -212,7 +203,6 @@ public class ColorTemperatureController extends LiveDisplayFeature { if (isTransitioning()) { // fire again in a minute - mHandler.removeCallbacks(mTransitionRunnable); mHandler.postDelayed(mTransitionRunnable, DateUtils.MINUTE_IN_MILLIS); } } @@ -270,10 +260,6 @@ public class ColorTemperatureController extends LiveDisplayFeature { } private synchronized void setDisplayTemperature(int temperature) { - if (temperature < 0) { - // We're in not yet initialized state, silently ignore. - return; - } if (!mColorTemperatureRange.contains(temperature)) { Slog.e(TAG, "Color temperature out of range: " + temperature); return; @@ -296,47 +282,50 @@ public class ColorTemperatureController extends LiveDisplayFeature { } } + /** + * Where is the sun anyway? This calculation determines day or night, and scales + * the value around sunset/sunrise for a smooth transition. + * + * @param now + * @param sunset + * @param sunrise + * @return float between 0 and 1 + */ + private static float adj(long now, long sunset, long sunrise) { + if (sunset < 0 || sunrise < 0 + || now < sunset || now > (sunrise + TWILIGHT_ADJUSTMENT_TIME)) { + return 1.0f; + } + + if (now <= (sunset + TWILIGHT_ADJUSTMENT_TIME)) { + return MathUtils.lerp(1.0f, 0.0f, + (float) (now - sunset) / TWILIGHT_ADJUSTMENT_TIME); + } + + if (now >= sunrise) { + return MathUtils.lerp(1.0f, 0.0f, + (float) ((sunrise + TWILIGHT_ADJUSTMENT_TIME) - now) / TWILIGHT_ADJUSTMENT_TIME); + } + + return 0.0f; + } + /** * Determine the color temperature we should use for the display based on * the position of the sun. * - * @return color temperature in Kelvin or -1 if current state can't be determined. + * @return color temperature in Kelvin */ private int getTwilightK() { + float adjustment = 1.0f; final TwilightState twilight = getTwilight(); - if (twilight == null) { - return -1; + + if (twilight != null) { + final long now = System.currentTimeMillis(); + adjustment = adj(now, twilight.getYesterdaySunset(), twilight.getTodaySunrise()) * + adj(now, twilight.getTodaySunset(), twilight.getTomorrowSunrise()); } - final long now = System.currentTimeMillis(); - final long sunrise = twilight.sunriseTimeMillis(); - final long sunset = twilight.sunsetTimeMillis(); - final float adjustment; - - // Sanity checks - if (sunrise <= 0 || sunset <= 0) { - return -1; - } - - if (now >= sunrise && now < sunset) { - // It's daytime - if (now < sunrise + TWILIGHT_ADJUSTMENT_TIME) { - adjustment = MathUtils.lerp(0.0f, 1.0f, (float) (now - sunrise) / - TWILIGHT_ADJUSTMENT_TIME); - } else { - adjustment = 1.0f; - } - } else if (now >= sunset && now < sunrise) { - // It's nighttime - if (now < sunset + TWILIGHT_ADJUSTMENT_TIME) { - adjustment = MathUtils.lerp(1.0f, 0.0f, (float) (now - sunset) / - TWILIGHT_ADJUSTMENT_TIME); - } else { - adjustment = 0.0f; - } - } else { - return -1; - } return (int)MathUtils.lerp(mNightTemperature, mDayTemperature, adjustment); } diff --git a/lineage/lib/main/java/org/lineageos/platform/internal/display/LiveDisplayFeature.java b/lineage/lib/main/java/org/lineageos/platform/internal/display/LiveDisplayFeature.java index 635183be..d3279430 100644 --- a/lineage/lib/main/java/org/lineageos/platform/internal/display/LiveDisplayFeature.java +++ b/lineage/lib/main/java/org/lineageos/platform/internal/display/LiveDisplayFeature.java @@ -22,10 +22,9 @@ import android.os.Handler; import android.os.UserHandle; import android.util.Log; -import com.android.server.twilight.TwilightState; - import org.lineageos.platform.internal.common.UserContentObserver; import org.lineageos.platform.internal.display.LiveDisplayService.State; +import org.lineageos.platform.internal.display.TwilightTracker.TwilightState; import java.io.PrintWriter; import java.util.BitSet; diff --git a/lineage/lib/main/java/org/lineageos/platform/internal/display/LiveDisplayService.java b/lineage/lib/main/java/org/lineageos/platform/internal/display/LiveDisplayService.java index 60e82d9d..bc8f2ec3 100644 --- a/lineage/lib/main/java/org/lineageos/platform/internal/display/LiveDisplayService.java +++ b/lineage/lib/main/java/org/lineageos/platform/internal/display/LiveDisplayService.java @@ -1,6 +1,5 @@ /* * Copyright (C) 2016 The CyanogenMod Project - * Copyright (C) 2018 The LineageOS Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,12 +35,11 @@ import android.view.Display; import com.android.server.LocalServices; import com.android.server.ServiceThread; import com.android.server.power.BatterySaverPolicy.ServiceType; -import com.android.server.twilight.TwilightListener; -import com.android.server.twilight.TwilightManager; -import com.android.server.twilight.TwilightState; import org.lineageos.platform.internal.LineageSystemService; import org.lineageos.platform.internal.common.UserContentObserver; +import org.lineageos.platform.internal.display.TwilightTracker.TwilightListener; +import org.lineageos.platform.internal.display.TwilightTracker.TwilightState; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -83,7 +81,7 @@ public class LiveDisplayService extends LineageSystemService { private DisplayManager mDisplayManager; private ModeObserver mModeObserver; - private TwilightManager mTwilightManager; + private final TwilightTracker mTwilightTracker; private boolean mAwaitingNudge = true; private boolean mSunset = false; @@ -132,6 +130,8 @@ public class LiveDisplayService extends LineageSystemService { Process.THREAD_PRIORITY_DEFAULT, false /*allowIo*/); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); + + mTwilightTracker = new TwilightTracker(context); } @Override @@ -202,9 +202,8 @@ public class LiveDisplayService extends LineageSystemService { mState.mLowPowerMode = pmi.getLowPowerState(SERVICE_TYPE_DUMMY).globalBatterySaverEnabled; - mTwilightManager = getLocalService(TwilightManager.class); - mTwilightManager.registerListener(mTwilightListener, mHandler); - mState.mTwilight = mTwilightManager.getLastTwilightState(); + mTwilightTracker.registerListener(mTwilightListener, mHandler); + mState.mTwilight = mTwilightTracker.getCurrentState(); if (mConfig.hasModeSupport()) { mModeObserver = new ModeObserver(mHandler); @@ -374,7 +373,7 @@ public class LiveDisplayService extends LineageSystemService { @Override public boolean isNight() { - final TwilightState twilight = mTwilightManager.getLastTwilightState(); + final TwilightState twilight = mTwilightTracker.getCurrentState(); return twilight != null && twilight.isNight(); } }; @@ -466,8 +465,8 @@ public class LiveDisplayService extends LineageSystemService { // Night watchman private final TwilightListener mTwilightListener = new TwilightListener() { @Override - public void onTwilightStateChanged(TwilightState state) { - mState.mTwilight = state; + public void onTwilightStateChanged() { + mState.mTwilight = mTwilightTracker.getCurrentState(); updateFeatures(TWILIGHT_CHANGED); nudge(); } @@ -512,7 +511,7 @@ public class LiveDisplayService extends LineageSystemService { * @param state */ private void nudge() { - final TwilightState twilight = mTwilightManager.getLastTwilightState(); + final TwilightState twilight = mTwilightTracker.getCurrentState(); if (!mAwaitingNudge || twilight == null) { return; } diff --git a/lineage/lib/main/java/org/lineageos/platform/internal/display/TwilightCalculator.java b/lineage/lib/main/java/org/lineageos/platform/internal/display/TwilightCalculator.java new file mode 100644 index 00000000..31c94c7b --- /dev/null +++ b/lineage/lib/main/java/org/lineageos/platform/internal/display/TwilightCalculator.java @@ -0,0 +1,123 @@ +/* + * 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 org.lineageos.platform.internal.display; + +import android.text.format.DateUtils; +import android.util.FloatMath; + +/** @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 float trueAnomaly = meanAnomaly + C1 * FloatMath.sin(meanAnomaly) + C2 + * FloatMath.sin(2 * meanAnomaly) + C3 * FloatMath.sin(3 * meanAnomaly); + + // ecliptic longitude + final float solarLng = trueAnomaly + 1.796593063f + (float) 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.0053f * FloatMath.sin(meanAnomaly) + + -0.0069f * FloatMath.sin(2 * solarLng); + + // declination of sun + double solarDec = Math.asin(FloatMath.sin(solarLng) * FloatMath.sin(OBLIQUITY)); + + final double latRad = latiude * DEGREES_TO_RADIANS; + + double cosHourAngle = (FloatMath.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/lineage/lib/main/java/org/lineageos/platform/internal/display/TwilightTracker.java b/lineage/lib/main/java/org/lineageos/platform/internal/display/TwilightTracker.java new file mode 100644 index 00000000..e64f28d4 --- /dev/null +++ b/lineage/lib/main/java/org/lineageos/platform/internal/display/TwilightTracker.java @@ -0,0 +1,562 @@ +/* + * Copyright (C) 2012 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 org.lineageos.platform.internal.display; + +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.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.text.format.DateUtils; +import android.text.format.Time; +import android.util.Slog; + +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; + +import libcore.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 TwilightTracker { + private static final String TAG = "TwilightTracker"; + private static final boolean DEBUG = false; + private static final String ACTION_UPDATE_TWILIGHT_STATE = + "lineageos.platform.intent.action.UPDATE_TWILIGHT_STATE"; + + private final Object mLock = new Object(); + + private final AlarmManager mAlarmManager; + private final LocationManager mLocationManager; + private final LocationHandler mLocationHandler; + + private final ArrayList mListeners = + new ArrayList(); + + private TwilightState mTwilightState; + + private final Context mContext; + + public TwilightTracker(Context context) { + mContext = context; + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + mLocationManager = (LocationManager) mContext.getSystemService( + Context.LOCATION_SERVICE); + mLocationHandler = new LocationHandler(); + + IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + filter.addAction(ACTION_UPDATE_TWILIGHT_STATE); + mContext.registerReceiver(mUpdateLocationReceiver, filter); + } + + /** + * Gets the current twilight state. + * + * @return The current twilight state, or null if no information is available. + */ + public TwilightState getCurrentState() { + synchronized (mLock) { + return mTwilightState; + } + } + + /** + * Listens for twilight time. + * + * @param listener The listener. + */ + public void registerListener(TwilightListener listener, Handler handler) { + synchronized (mLock) { + mListeners.add(new TwilightListenerRecord(listener, handler)); + + if (mListeners.size() == 1) { + mLocationHandler.enableLocationUpdates(); + } + } + } + + + private void setTwilightState(TwilightState state) { + synchronized (mLock) { + if (!Objects.equal(mTwilightState, state)) { + if (DEBUG) { + Slog.d(TAG, "Twilight state changed: " + state); + } + + mTwilightState = state; + + final int listenerLen = mListeners.size(); + for (int i = 0; i < listenerLen; i++) { + mListeners.get(i).postUpdate(); + } + } + } + } + + // 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 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 boolean mPassiveListenerEnabled; + private boolean mNetworkListenerEnabled; + private boolean mDidFirstInit; + private long mLastNetworkRegisterTime = -MIN_LOCATION_UPDATE_MS; + private long mLastUpdateInterval; + private Location mLocation; + private final TwilightCalculator mTwilightCalculator = new TwilightCalculator(); + + 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 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); + } + + if (!networkLocationEnabled && mLocation == null) { + if (DEBUG) { + Slog.d(TAG, "Network location unavailable"); + } + retrieveLocation(); + } + break; + + case MSG_DO_TWILIGHT_UPDATE: + updateTwilightState(); + break; + } + } + + private void retrieveLocation() { + Location location = null; + final Iterator 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 yesterday's twilight + mTwilightCalculator.calculateTwilight(now - DateUtils.DAY_IN_MILLIS, + mLocation.getLatitude(), mLocation.getLongitude()); + final long yesterdaySunset = mTwilightCalculator.mSunset; + + // 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; + + // set twilight state + TwilightState state = new TwilightState(isNight, yesterdaySunset, + todaySunrise, todaySunset, tomorrowSunrise); + 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 (now > todaySunset) { + nextUpdate += tomorrowSunrise; + } else if (now > todaySunrise) { + nextUpdate += todaySunset; + } else { + nextUpdate += todaySunrise; + } + } + + if (DEBUG) { + Slog.d(TAG, "Next update in " + (nextUpdate - now) + " ms"); + } + + Intent updateIntent = new Intent(ACTION_UPDATE_TWILIGHT_STATE); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0, updateIntent, 0); + mAlarmManager.cancel(pendingIntent); + mAlarmManager.setExact(AlarmManager.RTC, nextUpdate, pendingIntent); + } + } + + private final BroadcastReceiver mUpdateLocationReceiver = 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(); + } + }; + + // A LocationListener to initialize the network location provider. The location updates + // are handled through the passive location provider. + private final LocationListener mEmptyLocationListener = new LocationListener() { + public void onLocationChanged(Location location) { + } + + public void onProviderDisabled(String provider) { + } + + public void onProviderEnabled(String provider) { + } + + public void onStatusChanged(String provider, int status, Bundle extras) { + } + }; + + private final LocationListener mLocationListener = new LocationListener() { + public void onLocationChanged(Location location) { + mLocationHandler.processNewLocation(location); + } + + public void onProviderDisabled(String provider) { + } + + public void onProviderEnabled(String provider) { + } + + public void onStatusChanged(String provider, int status, Bundle extras) { + } + }; + + public static class TwilightState { + private final boolean mIsNight; + private final long mYesterdaySunset; + private final long mTodaySunrise; + private final long mTodaySunset; + private final long mTomorrowSunrise; + + TwilightState(boolean isNight, + long yesterdaySunset, + long todaySunrise, long todaySunset, + long tomorrowSunrise) { + mIsNight = isNight; + mYesterdaySunset = yesterdaySunset; + mTodaySunrise = todaySunrise; + mTodaySunset = todaySunset; + mTomorrowSunrise = tomorrowSunrise; + } + + /** + * Returns true if it is currently night time. + */ + public boolean isNight() { + return mIsNight; + } + + /** + * Returns the time of yesterday's sunset in the System.currentTimeMillis() timebase, + * or -1 if the sun never sets. + */ + public long getYesterdaySunset() { + return mYesterdaySunset; + } + + /** + * Returns the time of today's sunrise in the System.currentTimeMillis() timebase, + * or -1 if the sun never rises. + */ + public long getTodaySunrise() { + return mTodaySunrise; + } + + /** + * Returns the time of today's sunset in the System.currentTimeMillis() timebase, + * or -1 if the sun never sets. + */ + public long getTodaySunset() { + return mTodaySunset; + } + + /** + * Returns the time of tomorrow's sunrise in the System.currentTimeMillis() timebase, + * or -1 if the sun never rises. + */ + public long getTomorrowSunrise() { + return mTomorrowSunrise; + } + + @Override + public boolean equals(Object o) { + return o instanceof TwilightState && equals((TwilightState) o); + } + + public boolean equals(TwilightState other) { + return other != null + && mIsNight == other.mIsNight + && mYesterdaySunset == other.mYesterdaySunset + && mTodaySunrise == other.mTodaySunrise + && mTodaySunset == other.mTodaySunset + && mTomorrowSunrise == other.mTomorrowSunrise; + } + + @Override + public int hashCode() { + return 0; // don't care + } + + @Override + public String toString() { + DateFormat f = DateFormat.getDateTimeInstance(); + return "{TwilightState: isNight=" + mIsNight + + ", mYesterdaySunset=" + f.format(new Date(mYesterdaySunset)) + + ", mTodaySunrise=" + f.format(new Date(mTodaySunrise)) + + ", mTodaySunset=" + f.format(new Date(mTodaySunset)) + + ", mTomorrowSunrise=" + f.format(new Date(mTomorrowSunrise)) + + "}"; + + } + } + + public interface TwilightListener { + void onTwilightStateChanged(); + } + + private static class TwilightListenerRecord implements Runnable { + private final TwilightListener mListener; + private final Handler mHandler; + + public TwilightListenerRecord(TwilightListener listener, Handler handler) { + mListener = listener; + mHandler = handler; + } + + public void postUpdate() { + mHandler.post(this); + } + + @Override + public void run() { + mListener.onTwilightStateChanged(); + } + } +}