| /* | |
| * 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 com.android.server; | |
| 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 TwilightService { | |
| 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 final Context mContext; | |
| private final AlarmManager mAlarmManager; | |
| private final LocationManager mLocationManager; | |
| private final LocationHandler mLocationHandler; | |
| private final Object mLock = new Object(); | |
| private final ArrayList<TwilightListenerRecord> mListeners = | |
| new ArrayList<TwilightListenerRecord>(); | |
| private boolean mSystemReady; | |
| private TwilightState mTwilightState; | |
| public TwilightService(Context context) { | |
| mContext = context; | |
| mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); | |
| mLocationManager = (LocationManager)mContext.getSystemService(Context.LOCATION_SERVICE); | |
| mLocationHandler = new LocationHandler(); | |
| } | |
| void systemReady() { | |
| synchronized (mLock) { | |
| mSystemReady = true; | |
| 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); | |
| if (!mListeners.isEmpty()) { | |
| mLocationHandler.enableLocationUpdates(); | |
| } | |
| } | |
| } | |
| /** | |
| * 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. | |
| * @param handler The handler on which to post calls into the listener. | |
| */ | |
| public void registerListener(TwilightListener listener, Handler handler) { | |
| synchronized (mLock) { | |
| mListeners.add(new TwilightListenerRecord(listener, handler)); | |
| if (mSystemReady && 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; | |
| int count = mListeners.size(); | |
| for (int i = 0; i < count; i++) { | |
| mListeners.get(i).post(); | |
| } | |
| } | |
| } | |
| } | |
| // 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; | |
| } | |
| /** | |
| * Describes whether it is day or night. | |
| * This object is immutable. | |
| */ | |
| public static final 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)) | |
| + "}"; | |
| } | |
| } | |
| /** | |
| * Listener for changes in twilight state. | |
| */ | |
| public interface TwilightListener { | |
| public void onTwilightStateChanged(); | |
| } | |
| private static final class TwilightListenerRecord implements Runnable { | |
| private final TwilightListener mListener; | |
| private final Handler mHandler; | |
| public TwilightListenerRecord(TwilightListener listener, Handler handler) { | |
| mListener = listener; | |
| mHandler = handler; | |
| } | |
| public void post() { | |
| mHandler.post(this); | |
| } | |
| @Override | |
| public void run() { | |
| mListener.onTwilightStateChanged(); | |
| } | |
| } | |
| 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); | |
| } | |
| break; | |
| case MSG_DO_TWILIGHT_UPDATE: | |
| updateTwilightState(); | |
| break; | |
| } | |
| } | |
| 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 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.set(AlarmManager.RTC_WAKEUP, 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) { | |
| } | |
| }; | |
| } |