| /* |
| * 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.twilight; |
| |
| import com.android.server.SystemService; |
| import com.android.server.TwilightCalculator; |
| |
| 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.util.ArrayList; |
| 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 extends SystemService { |
| static final String TAG = "TwilightService"; |
| static final boolean DEBUG = false; |
| static final String ACTION_UPDATE_TWILIGHT_STATE = |
| "com.android.server.action.UPDATE_TWILIGHT_STATE"; |
| |
| final Object mLock = new Object(); |
| |
| AlarmManager mAlarmManager; |
| LocationManager mLocationManager; |
| LocationHandler mLocationHandler; |
| |
| final ArrayList<TwilightListenerRecord> mListeners = |
| new ArrayList<TwilightListenerRecord>(); |
| |
| TwilightState mTwilightState; |
| |
| @Override |
| public void onStart() { |
| mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); |
| mLocationManager = (LocationManager) getContext().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); |
| getContext().registerReceiver(mUpdateLocationReceiver, filter); |
| |
| publishLocalService(TwilightManager.class, mService); |
| } |
| |
| 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(); |
| } |
| |
| } |
| |
| private final TwilightManager mService = new TwilightManager() { |
| /** |
| * Gets the current twilight state. |
| * |
| * @return The current twilight state, or null if no information is available. |
| */ |
| @Override |
| public TwilightState getCurrentState() { |
| synchronized (mLock) { |
| return mTwilightState; |
| } |
| } |
| |
| /** |
| * Listens for twilight time. |
| * |
| * @param listener The listener. |
| */ |
| @Override |
| 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); |
| } |
| 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( |
| getContext(), 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) { |
| } |
| }; |
| } |