diff --git a/services/java/com/android/server/DockObserver.java b/services/java/com/android/server/DockObserver.java index d2804750059b9..f0bab81f6b4d1 100644 --- a/services/java/com/android/server/DockObserver.java +++ b/services/java/com/android/server/DockObserver.java @@ -18,10 +18,12 @@ package com.android.server; import android.app.Activity; import android.app.ActivityManagerNative; +import android.app.AlarmManager; import android.app.IActivityManager; import android.app.IUiModeManager; import android.app.KeyguardManager; import android.app.StatusBarManager; +import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.ActivityNotFoundException; @@ -29,11 +31,18 @@ import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Configuration; +import android.location.Criteria; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; import android.os.Binder; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.RemoteException; @@ -42,6 +51,8 @@ import android.os.SystemClock; import android.os.UEventObserver; import android.provider.Settings; import android.server.BluetoothService; +import android.text.format.DateUtils; +import android.text.format.Time; import android.util.Log; import com.android.internal.widget.LockPatternUtils; @@ -59,10 +70,26 @@ class DockObserver extends UEventObserver { private static final String DOCK_UEVENT_MATCH = "DEVPATH=/devices/virtual/switch/dock"; private static final String DOCK_STATE_PATH = "/sys/class/switch/dock/state"; + private static final String KEY_LAST_UPDATE_INTERVAL = "LAST_UPDATE_INTERVAL"; + + private static final int MSG_DOCK_STATE = 0; + private static final int MSG_UPDATE_TWILIGHT = 1; + private static final int MSG_ENABLE_LOCATION_UPDATES = 2; + public static final int MODE_NIGHT_AUTO = Configuration.UI_MODE_NIGHT_MASK >> 4; public static final int MODE_NIGHT_NO = Configuration.UI_MODE_NIGHT_NO >> 4; public static final int MODE_NIGHT_YES = Configuration.UI_MODE_NIGHT_YES >> 4; + private static final long 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 = 5 * DateUtils.MINUTE_IN_MILLIS; + // velocity for estimating a potential movement: 150km/h + private static final float MAX_VELOCITY_M_MS = 150 / 3600; + private static final double FACTOR_GMT_OFFSET_LONGITUDE = 1000.0 * 360.0 / DateUtils.DAY_IN_MILLIS; + + private static final String ACTION_UPDATE_NIGHT_MODE = "com.android.server.action.UPDATE_NIGHT_MODE"; + private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; private int mPreviousDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; @@ -79,7 +106,11 @@ class DockObserver extends UEventObserver { private boolean mKeyguardDisabled; private LockPatternUtils mLockPatternUtils; - private StatusBarManager mStatusBarManager; + private AlarmManager mAlarmManager; + + private LocationManager mLocationManager; + private Location mLocation; + private StatusBarManager mStatusBarManager; // The broadcast receiver which receives the result of the ordered broadcast sent when // the dock state changes. The original ordered broadcast is sent with an initial result @@ -115,6 +146,81 @@ class DockObserver extends UEventObserver { } }; + private final BroadcastReceiver mTwilightUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mCarModeEnabled && mNightMode == MODE_NIGHT_AUTO) { + mHandler.sendEmptyMessage(MSG_UPDATE_TWILIGHT); + } + } + }; + + private final LocationListener mLocationListener = new LocationListener() { + + public void onLocationChanged(Location location) { + updateLocation(location); + } + + public void onProviderDisabled(String provider) { + } + + public void onProviderEnabled(String provider) { + } + + public void onStatusChanged(String provider, int status, Bundle extras) { + // If the network location is no longer available check for a GPS fix + // and try to update the location. + if (provider == LocationManager.NETWORK_PROVIDER && + status != LocationProvider.AVAILABLE) { + updateLocation(mLocation); + } + } + + private void updateLocation(Location location) { + location = DockObserver.chooseBestLocation(location, + mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)); + if (hasMoved(location)) { + synchronized (this) { + mLocation = location; + } + if (mCarModeEnabled && mNightMode == MODE_NIGHT_AUTO) { + mHandler.sendEmptyMessage(MSG_UPDATE_TWILIGHT); + } + } + } + + /* + * The user has moved if the accuracy circles of the two locations + * don't overlap. + */ + private boolean hasMoved(Location location) { + if (location == null) { + return false; + } + if (mLocation == null) { + return true; + } + + /* if new location is older than the current one, the devices hasn't + * moved. + */ + if (location.getTime() < mLocation.getTime()) { + return false; + } + + /* Get the distance between the two points */ + float distance = mLocation.distanceTo(location); + + /* Get the total accuracy radius for both locations */ + float totalAccuracy = mLocation.getAccuracy() + location.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; + } + }; + public DockObserver(Context context, PowerManagerService pm) { mContext = context; mPowerManager = pm; @@ -123,6 +229,13 @@ class DockObserver extends UEventObserver { ServiceManager.addService("uimode", mBinder); + mAlarmManager = + (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); + mLocationManager = + (LocationManager)mContext.getSystemService(Context.LOCATION_SERVICE); + mContext.registerReceiver(mTwilightUpdateReceiver, + new IntentFilter(ACTION_UPDATE_NIGHT_MODE)); + startObserving(DOCK_UEVENT_MATCH); } @@ -190,83 +303,161 @@ class DockObserver extends UEventObserver { update(); } mSystemReady = true; + mHandler.sendEmptyMessage(MSG_ENABLE_LOCATION_UPDATES); } } private final void update() { - mHandler.sendEmptyMessage(0); + mHandler.sendEmptyMessage(MSG_DOCK_STATE); } private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { - synchronized (this) { - Log.i(TAG, "Dock state changed: " + mDockState); + switch (msg.what) { + case MSG_DOCK_STATE: + synchronized (this) { + Log.i(TAG, "Dock state changed: " + mDockState); - final ContentResolver cr = mContext.getContentResolver(); + final ContentResolver cr = mContext.getContentResolver(); - if (Settings.Secure.getInt(cr, - Settings.Secure.DEVICE_PROVISIONED, 0) == 0) { - Log.i(TAG, "Device not provisioned, skipping dock broadcast"); - return; - } - // Pack up the values and broadcast them to everyone - Intent intent = new Intent(Intent.ACTION_DOCK_EVENT); - intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); - if (mCarModeEnabled && mDockState != Intent.EXTRA_DOCK_STATE_CAR) { - // Pretend to be in DOCK_STATE_CAR. - intent.putExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_CAR); - } else { - intent.putExtra(Intent.EXTRA_DOCK_STATE, mDockState); - } - intent.putExtra(Intent.EXTRA_CAR_MODE_ENABLED, mCarModeEnabled); - - // Check if this is Bluetooth Dock - String address = BluetoothService.readDockBluetoothAddress(); - if (address != null) - intent.putExtra(BluetoothDevice.EXTRA_DEVICE, - BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address)); - - // User feedback to confirm dock connection. Particularly - // useful for flaky contact pins... - if (Settings.System.getInt(cr, - Settings.System.DOCK_SOUNDS_ENABLED, 1) == 1) - { - String whichSound = null; - if (mDockState == Intent.EXTRA_DOCK_STATE_UNDOCKED) { - if (mPreviousDockState == Intent.EXTRA_DOCK_STATE_DESK) { - whichSound = Settings.System.DESK_UNDOCK_SOUND; - } else if (mPreviousDockState == Intent.EXTRA_DOCK_STATE_CAR) { - whichSound = Settings.System.CAR_UNDOCK_SOUND; + if (Settings.Secure.getInt(cr, + Settings.Secure.DEVICE_PROVISIONED, 0) == 0) { + Log.i(TAG, "Device not provisioned, skipping dock broadcast"); + return; } - } else { - if (mDockState == Intent.EXTRA_DOCK_STATE_DESK) { - whichSound = Settings.System.DESK_DOCK_SOUND; - } else if (mDockState == Intent.EXTRA_DOCK_STATE_CAR) { - whichSound = Settings.System.CAR_DOCK_SOUND; + // Pack up the values and broadcast them to everyone + Intent intent = new Intent(Intent.ACTION_DOCK_EVENT); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + if (mCarModeEnabled && mDockState != Intent.EXTRA_DOCK_STATE_CAR) { + // Pretend to be in DOCK_STATE_CAR. + intent.putExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_CAR); + } else { + intent.putExtra(Intent.EXTRA_DOCK_STATE, mDockState); } + intent.putExtra(Intent.EXTRA_CAR_MODE_ENABLED, mCarModeEnabled); + + // Check if this is Bluetooth Dock + String address = BluetoothService.readDockBluetoothAddress(); + if (address != null) + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, + BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address)); + + // User feedback to confirm dock connection. Particularly + // useful for flaky contact pins... + if (Settings.System.getInt(cr, + Settings.System.DOCK_SOUNDS_ENABLED, 1) == 1) + { + String whichSound = null; + if (mDockState == Intent.EXTRA_DOCK_STATE_UNDOCKED) { + if (mPreviousDockState == Intent.EXTRA_DOCK_STATE_DESK) { + whichSound = Settings.System.DESK_UNDOCK_SOUND; + } else if (mPreviousDockState == Intent.EXTRA_DOCK_STATE_CAR) { + whichSound = Settings.System.CAR_UNDOCK_SOUND; + } + } else { + if (mDockState == Intent.EXTRA_DOCK_STATE_DESK) { + whichSound = Settings.System.DESK_DOCK_SOUND; + } else if (mDockState == Intent.EXTRA_DOCK_STATE_CAR) { + whichSound = Settings.System.CAR_DOCK_SOUND; + } + } + + if (whichSound != null) { + final String soundPath = Settings.System.getString(cr, whichSound); + if (soundPath != null) { + final Uri soundUri = Uri.parse("file://" + soundPath); + if (soundUri != null) { + final Ringtone sfx = RingtoneManager.getRingtone(mContext, soundUri); + if (sfx != null) sfx.play(); + } + } + } + } + + // Send the ordered broadcast; the result receiver will receive after all + // broadcasts have been sent. If any broadcast receiver changes the result + // code from the initial value of RESULT_OK, then the result receiver will + // not launch the corresponding dock application. This gives apps a chance + // to override the behavior and stay in their app even when the device is + // placed into a dock. + mContext.sendStickyOrderedBroadcast( + intent, mResultReceiver, null, Activity.RESULT_OK, null, null); + } - - if (whichSound != null) { - final String soundPath = Settings.System.getString(cr, whichSound); - if (soundPath != null) { - final Uri soundUri = Uri.parse("file://" + soundPath); - if (soundUri != null) { - final Ringtone sfx = RingtoneManager.getRingtone(mContext, soundUri); - if (sfx != null) sfx.play(); + break; + case MSG_UPDATE_TWILIGHT: + synchronized (this) { + if (mCarModeEnabled && mLocation != null && mNightMode == MODE_NIGHT_AUTO) { + try { + DockObserver.this.updateTwilight(); + } catch (RemoteException e) { + Log.w(TAG, "Unable to change night mode.", e); } } } - } + break; + case MSG_ENABLE_LOCATION_UPDATES: + if (mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { + mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, + LOCATION_UPDATE_MS, LOCATION_UPDATE_DISTANCE_METER, mLocationListener); + retrieveLocation(); + if (mLocation != null) { + try { + DockObserver.this.updateTwilight(); + } catch (RemoteException e) { + Log.w(TAG, "Unable to change night mode.", e); + } + } + } else { + long interval = msg.getData().getLong(KEY_LAST_UPDATE_INTERVAL); + interval *= 1.5; + if (interval == 0) { + interval = LOCATION_UPDATE_ENABLE_INTERVAL_MIN; + } else if (interval > LOCATION_UPDATE_ENABLE_INTERVAL_MAX) { + interval = LOCATION_UPDATE_ENABLE_INTERVAL_MAX; + } + Bundle bundle = new Bundle(); + bundle.putLong(KEY_LAST_UPDATE_INTERVAL, interval); + Message newMsg = mHandler.obtainMessage(MSG_ENABLE_LOCATION_UPDATES); + newMsg.setData(bundle); + mHandler.sendMessageDelayed(newMsg, interval); + } + break; + } + } - // Send the ordered broadcast; the result receiver will receive after all - // broadcasts have been sent. If any broadcast receiver changes the result - // code from the initial value of RESULT_OK, then the result receiver will - // not launch the corresponding dock application. This gives apps a chance - // to override the behavior and stay in their app even when the device is - // placed into a dock. - mContext.sendStickyOrderedBroadcast( - intent, mResultReceiver, null, Activity.RESULT_OK, null, null); + private void retrieveLocation() { + final Location gpsLocation = + mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + Location location; + Criteria criteria = new Criteria(); + criteria.setSpeedRequired(false); + criteria.setAltitudeRequired(false); + criteria.setBearingRequired(false); + final String bestProvider = mLocationManager.getBestProvider(criteria, true); + if (LocationManager.GPS_PROVIDER.equals(bestProvider)) { + location = gpsLocation; + } else { + location = DockObserver.chooseBestLocation(gpsLocation, + mLocationManager.getLastKnownLocation(bestProvider)); + } + // 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(59.95); + location.setAccuracy(417000.0f); + location.setTime(System.currentTimeMillis()); + } + synchronized (this) { + mLocation = location; } } }; @@ -274,10 +465,15 @@ class DockObserver extends UEventObserver { private void setCarMode(boolean enabled) throws RemoteException { mCarModeEnabled = enabled; if (enabled) { - setMode(Configuration.UI_MODE_TYPE_CAR, mNightMode); + if (mNightMode == MODE_NIGHT_AUTO) { + updateTwilight(); + } else { + setMode(Configuration.UI_MODE_TYPE_CAR, mNightMode << 4); + } } else { // Disabling the car mode clears the night mode. - setMode(Configuration.UI_MODE_TYPE_NORMAL, MODE_NIGHT_NO); + setMode(Configuration.UI_MODE_TYPE_NORMAL, + Configuration.UI_MODE_NIGHT_UNDEFINED); } if (mStatusBarManager == null) { @@ -290,38 +486,112 @@ class DockObserver extends UEventObserver { // the status bar should be totally disabled, the calls below will // have no effect until the device is unlocked. if (mStatusBarManager != null) { - mStatusBarManager.disable(enabled + mStatusBarManager.disable(enabled ? StatusBarManager.DISABLE_NOTIFICATION_TICKER : StatusBarManager.DISABLE_NONE); } } private void setMode(int modeType, int modeNight) throws RemoteException { + long ident = Binder.clearCallingIdentity(); final IActivityManager am = ActivityManagerNative.getDefault(); Configuration config = am.getConfiguration(); - if (config.uiMode != (modeType | modeNight)) { config.uiMode = modeType | modeNight; - long ident = Binder.clearCallingIdentity(); am.updateConfiguration(config); - Binder.restoreCallingIdentity(ident); } + Binder.restoreCallingIdentity(ident); } private void setNightMode(int mode) throws RemoteException { - mNightMode = mode; - switch (mode) { - case MODE_NIGHT_NO: - case MODE_NIGHT_YES: - setMode(Configuration.UI_MODE_TYPE_CAR, mode << 4); - break; - case MODE_NIGHT_AUTO: - // FIXME: not yet supported, this functionality will be - // added in a separate change. - break; - default: - setMode(Configuration.UI_MODE_TYPE_CAR, MODE_NIGHT_NO << 4); - break; + if (mNightMode != mode) { + mNightMode = mode; + switch (mode) { + case MODE_NIGHT_NO: + case MODE_NIGHT_YES: + setMode(Configuration.UI_MODE_TYPE_CAR, mode << 4); + break; + case MODE_NIGHT_AUTO: + long ident = Binder.clearCallingIdentity(); + updateTwilight(); + Binder.restoreCallingIdentity(ident); + break; + default: + setMode(Configuration.UI_MODE_TYPE_CAR, MODE_NIGHT_NO << 4); + break; + } + } + } + + private void updateTwilight() throws RemoteException { + synchronized (this) { + if (mLocation == null) { + return; + } + final long currentTime = System.currentTimeMillis(); + int nightMode; + // calculate current twilight + TwilightCalculator tw = new TwilightCalculator(); + tw.calculateTwilight(currentTime, + mLocation.getLatitude(), mLocation.getLongitude()); + if (tw.mState == TwilightCalculator.DAY) { + nightMode = MODE_NIGHT_NO; + } else { + nightMode = MODE_NIGHT_YES; + } + + // schedule next update + final int mLastTwilightState = tw.mState; + // add some extra time to be on the save side. + long nextUpdate = DateUtils.MINUTE_IN_MILLIS; + if (currentTime > tw.mSunset) { + // next update should be on the following day + tw.calculateTwilight(currentTime + + DateUtils.DAY_IN_MILLIS, mLocation.getLatitude(), + mLocation.getLongitude()); + } + + if (mLastTwilightState == TwilightCalculator.NIGHT) { + nextUpdate += tw.mSunrise; + } else { + nextUpdate += tw.mSunset; + } + + Intent updateIntent = new Intent(ACTION_UPDATE_NIGHT_MODE); + PendingIntent pendingIntent = + PendingIntent.getBroadcast(mContext, 0, updateIntent, 0); + mAlarmManager.cancel(pendingIntent); + mAlarmManager.set(AlarmManager.RTC_WAKEUP, nextUpdate, pendingIntent); + + // set current mode + setMode(Configuration.UI_MODE_TYPE_CAR, nightMode << 4); + } + } + + /** + * Check which of two locations is better by comparing the distance a device + * could have cover since the last timestamp of the location. + * + * @param location first location + * @param otherLocation second location + * @return one of the two locations + */ + protected static Location chooseBestLocation(Location location, Location otherLocation) { + if (location == null) { + return otherLocation; + } + if (otherLocation == null) { + return location; + } + final long currentTime = System.currentTimeMillis(); + float gpsPotentialMove = MAX_VELOCITY_M_MS * (currentTime - location.getTime()) + + location.getAccuracy(); + float otherPotentialMove = MAX_VELOCITY_M_MS * (currentTime - otherLocation.getTime()) + + otherLocation.getAccuracy(); + if (gpsPotentialMove < otherPotentialMove) { + return location; + } else { + return otherLocation; } } diff --git a/services/java/com/android/server/TwilightCalculator.java b/services/java/com/android/server/TwilightCalculator.java new file mode 100644 index 0000000000000..a8f67d88bbbd2 --- /dev/null +++ b/services/java/com/android/server/TwilightCalculator.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2010 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.text.format.DateUtils; +import android.util.FloatMath; + +/** @hide */ +public class TwilightCalculator { + + /** Value of {@link #mState} if it is currently day */ + public static final int DAY = 0; + + /** Value of {@link #mState} if it is currently night */ + public static final int NIGHT = 1; + + private static final float DEGREES_TO_RADIANS = (float) (Math.PI / 180.0f); + + // element for calculating solar transit. + private static final float J0 = 0.0009f; + + // correction for civil twilight + private static final float ALTIDUTE_CORRECTION_CIVIL_TWILIGHT = -0.104719755f; + + // coefficients for calculating Equation of Center. + private static final float C1 = 0.0334196f; + private static final float C2 = 0.000349066f; + private static final float C3 = 0.000005236f; + + private static final float OBLIQUITY = 0.40927971f; + + // Java time on Jan 1, 2000 12:00 UTC. + private static final long UTC_2000 = 946728000000L; + + /** Time of sunset (civil twilight) in milliseconds. */ + public long mSunset; + + /** Time of sunrise (civil twilight) in milliseconds. */ + public long mSunrise; + + /** Current state */ + public int mState; + + /** + * calculates the civil twilight bases on time and geo-coordinates. + * + * @param time time in milliseconds. + * @param latiude latitude in degrees. + * @param longitude latitude in degrees. + */ + public void calculateTwilight(long time, double latiude, double longitude) { + final float daysSince2000 = (float) (time - UTC_2000) / DateUtils.DAY_IN_MILLIS; + + // mean anomaly + final float meanAnomaly = 6.240059968f + daysSince2000 * 0.01720197f; + + // true anomaly + final float trueAnomaly = meanAnomaly + C1 * FloatMath.sin(meanAnomaly) + C2 + * FloatMath.sin(2 * meanAnomaly) + C3 * FloatMath.sin(3 * meanAnomaly); + + // ecliptic longitude + final float solarLng = trueAnomaly + 1.796593063f + (float) Math.PI; + + // solar transit in days since 2000 + final double arcLongitude = -longitude / 360; + float n = Math.round(daysSince2000 - J0 - arcLongitude); + double solarTransitJ2000 = n + J0 + arcLongitude + 0.0053f * FloatMath.sin(meanAnomaly) + + -0.0069f * FloatMath.sin(2 * solarLng); + + // declination of sun + double solarDec = Math.asin(FloatMath.sin(solarLng) * FloatMath.sin(OBLIQUITY)); + + final double latRad = latiude * DEGREES_TO_RADIANS; + float hourAngle = (float) (Math + .acos((FloatMath.sin(ALTIDUTE_CORRECTION_CIVIL_TWILIGHT) - Math.sin(latRad) + * Math.sin(solarDec)) + / (Math.cos(latRad) * Math.cos(solarDec))) / (2 * Math.PI)); + + mSunset = Math.round((solarTransitJ2000 + hourAngle) * DateUtils.DAY_IN_MILLIS) + UTC_2000; + mSunrise = Math.round((solarTransitJ2000 - hourAngle) * DateUtils.DAY_IN_MILLIS) + UTC_2000; + + if (mSunrise < time && mSunset > time) { + mState = DAY; + } else { + mState = NIGHT; + } + } + +}