| /* |
| * 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) { |
| } |
| }; |
| } |