573 lines
21 KiB
Java
573 lines
21 KiB
Java
/*
|
|
* 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) {
|
|
}
|
|
};
|
|
}
|