diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/display/ColorTemperatureController.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/display/ColorTemperatureController.java index 4fbf52f1..5d2794dc 100644 --- a/cm/lib/main/java/org/cyanogenmod/platform/internal/display/ColorTemperatureController.java +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/display/ColorTemperatureController.java @@ -15,11 +15,6 @@ */ package org.cyanogenmod.platform.internal.display; -import static cyanogenmod.hardware.LiveDisplayManager.MODE_AUTO; -import static cyanogenmod.hardware.LiveDisplayManager.MODE_DAY; -import static cyanogenmod.hardware.LiveDisplayManager.MODE_NIGHT; -import static cyanogenmod.hardware.LiveDisplayManager.MODE_OFF; - import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; @@ -31,7 +26,7 @@ import android.util.Range; import android.util.Slog; import android.view.animation.LinearInterpolator; -import com.android.server.twilight.TwilightState; +import org.cyanogenmod.platform.internal.display.TwilightTracker.TwilightState; import java.io.PrintWriter; import java.util.BitSet; @@ -41,6 +36,11 @@ import cyanogenmod.hardware.LiveDisplayManager; import cyanogenmod.providers.CMSettings; import cyanogenmod.util.ColorUtils; +import static cyanogenmod.hardware.LiveDisplayManager.MODE_AUTO; +import static cyanogenmod.hardware.LiveDisplayManager.MODE_DAY; +import static cyanogenmod.hardware.LiveDisplayManager.MODE_NIGHT; +import static cyanogenmod.hardware.LiveDisplayManager.MODE_OFF; + public class ColorTemperatureController extends LiveDisplayFeature { private final DisplayHardwareController mDisplayHardware; @@ -282,6 +282,34 @@ 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. @@ -293,7 +321,9 @@ public class ColorTemperatureController extends LiveDisplayFeature { final TwilightState twilight = getTwilight(); if (twilight != null) { - adjustment = twilight.getAmount(); + final long now = System.currentTimeMillis(); + adjustment = adj(now, twilight.getYesterdaySunset(), twilight.getTodaySunrise()) * + adj(now, twilight.getTodaySunset(), twilight.getTomorrowSunrise()); } return (int)MathUtils.lerp(mNightTemperature, mDayTemperature, adjustment); diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/display/LiveDisplayFeature.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/display/LiveDisplayFeature.java index 21a4849d..85e87d0a 100644 --- a/cm/lib/main/java/org/cyanogenmod/platform/internal/display/LiveDisplayFeature.java +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/display/LiveDisplayFeature.java @@ -15,11 +15,6 @@ */ package org.cyanogenmod.platform.internal.display; -import static org.cyanogenmod.platform.internal.display.LiveDisplayService.ALL_CHANGED; -import static org.cyanogenmod.platform.internal.display.LiveDisplayService.DISPLAY_CHANGED; -import static org.cyanogenmod.platform.internal.display.LiveDisplayService.MODE_CHANGED; -import static org.cyanogenmod.platform.internal.display.LiveDisplayService.TWILIGHT_CHANGED; - import android.content.ContentResolver; import android.content.Context; import android.net.Uri; @@ -27,16 +22,20 @@ import android.os.Handler; import android.os.UserHandle; import android.util.Log; -import com.android.server.twilight.TwilightState; - import org.cyanogenmod.platform.internal.common.UserContentObserver; import org.cyanogenmod.platform.internal.display.LiveDisplayService.State; +import org.cyanogenmod.platform.internal.display.TwilightTracker.TwilightState; import java.io.PrintWriter; import java.util.BitSet; import cyanogenmod.providers.CMSettings; +import static org.cyanogenmod.platform.internal.display.LiveDisplayService.ALL_CHANGED; +import static org.cyanogenmod.platform.internal.display.LiveDisplayService.DISPLAY_CHANGED; +import static org.cyanogenmod.platform.internal.display.LiveDisplayService.MODE_CHANGED; +import static org.cyanogenmod.platform.internal.display.LiveDisplayService.TWILIGHT_CHANGED; + public abstract class LiveDisplayFeature { protected static final String TAG = "LiveDisplay"; diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/display/LiveDisplayService.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/display/LiveDisplayService.java index ddd12172..3118e32b 100644 --- a/cm/lib/main/java/org/cyanogenmod/platform/internal/display/LiveDisplayService.java +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/display/LiveDisplayService.java @@ -15,13 +15,6 @@ */ package org.cyanogenmod.platform.internal.display; -import static cyanogenmod.hardware.LiveDisplayManager.FEATURE_MANAGED_OUTDOOR_MODE; -import static cyanogenmod.hardware.LiveDisplayManager.MODE_DAY; -import static cyanogenmod.hardware.LiveDisplayManager.MODE_FIRST; -import static cyanogenmod.hardware.LiveDisplayManager.MODE_LAST; -import static cyanogenmod.hardware.LiveDisplayManager.MODE_OFF; -import static cyanogenmod.hardware.LiveDisplayManager.MODE_OUTDOOR; - import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -45,15 +38,14 @@ import android.view.Display; import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.ServiceThread; -import com.android.server.twilight.TwilightListener; -import com.android.server.twilight.TwilightManager; -import com.android.server.twilight.TwilightState; import org.cyanogenmod.internal.util.QSConstants; import org.cyanogenmod.internal.util.QSUtils; -import org.cyanogenmod.platform.internal.common.UserContentObserver; import org.cyanogenmod.platform.internal.CMSystemService; import org.cyanogenmod.platform.internal.R; +import org.cyanogenmod.platform.internal.common.UserContentObserver; +import org.cyanogenmod.platform.internal.display.TwilightTracker.TwilightListener; +import org.cyanogenmod.platform.internal.display.TwilightTracker.TwilightState; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -70,6 +62,13 @@ import cyanogenmod.hardware.ILiveDisplayService; import cyanogenmod.hardware.LiveDisplayConfig; import cyanogenmod.providers.CMSettings; +import static cyanogenmod.hardware.LiveDisplayManager.FEATURE_MANAGED_OUTDOOR_MODE; +import static cyanogenmod.hardware.LiveDisplayManager.MODE_DAY; +import static cyanogenmod.hardware.LiveDisplayManager.MODE_FIRST; +import static cyanogenmod.hardware.LiveDisplayManager.MODE_LAST; +import static cyanogenmod.hardware.LiveDisplayManager.MODE_OFF; +import static cyanogenmod.hardware.LiveDisplayManager.MODE_OUTDOOR; + /** * LiveDisplay is an advanced set of features for improving * display quality under various ambient conditions. @@ -89,7 +88,7 @@ public class LiveDisplayService extends CMSystemService { private DisplayManager mDisplayManager; private ModeObserver mModeObserver; - private TwilightManager mTwilightManager; + private final TwilightTracker mTwilightTracker; private boolean mAwaitingNudge = true; private boolean mSunset = false; @@ -144,6 +143,8 @@ public class LiveDisplayService extends CMSystemService { mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); + mTwilightTracker = new TwilightTracker(context); + updateCustomTileEntries(); } @@ -213,11 +214,8 @@ public class LiveDisplayService extends CMSystemService { pmi.registerLowPowerModeObserver(mLowPowerModeListener); mState.mLowPowerMode = pmi.getLowPowerModeEnabled(); - mTwilightManager = LocalServices.getService(TwilightManager.class); - if (mTwilightManager != null) { - mTwilightManager.registerListener(mTwilightListener, mHandler); - mState.mTwilight = mTwilightManager.getCurrentState(); - } + mTwilightTracker.registerListener(mTwilightListener, mHandler); + mState.mTwilight = mTwilightTracker.getCurrentState(); if (mConfig.hasModeSupport()) { mModeObserver = new ModeObserver(mHandler); @@ -586,7 +584,7 @@ public class LiveDisplayService extends CMSystemService { private final TwilightListener mTwilightListener = new TwilightListener() { @Override public void onTwilightStateChanged() { - mState.mTwilight = mTwilightManager.getCurrentState(); + mState.mTwilight = mTwilightTracker.getCurrentState(); updateFeatures(TWILIGHT_CHANGED); nudge(); } @@ -631,7 +629,7 @@ public class LiveDisplayService extends CMSystemService { * @param state */ private void nudge() { - final TwilightState twilight = mTwilightManager.getCurrentState(); + final TwilightState twilight = mTwilightTracker.getCurrentState(); if (!mAwaitingNudge || twilight == null) { return; } diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/display/TwilightTracker.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/display/TwilightTracker.java new file mode 100644 index 00000000..259290f1 --- /dev/null +++ b/cm/lib/main/java/org/cyanogenmod/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.cyanogenmod.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 com.android.server.TwilightCalculator; + +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 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(TwilightTracker.class.getName()); + 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(mContext, TwilightTracker.class); + 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(); + } + } +}