Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e32c3453 authored by Mike Cleron's avatar Mike Cleron Committed by Android (Google) Code Review
Browse files

Merge "Add the automatic handling of night/notnight UI modes."

parents 0f5a434c bfca3a00
Loading
Loading
Loading
Loading
+349 −79
Original line number Diff line number Diff line
@@ -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,6 +106,10 @@ class DockObserver extends UEventObserver {
    private boolean mKeyguardDisabled;
    private LockPatternUtils mLockPatternUtils;

    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
@@ -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,16 +303,19 @@ 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) {
            switch (msg.what) {
                case MSG_DOCK_STATE:
                    synchronized (this) {
                        Log.i(TAG, "Dock state changed: " + mDockState);

@@ -267,6 +383,81 @@ class DockObserver extends UEventObserver {
                        // placed into a dock.
                        mContext.sendStickyOrderedBroadcast(
                                intent, mResultReceiver, null, Activity.RESULT_OK, null, null);

                    }
                    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;
            }
        }

        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) {
@@ -297,18 +493,18 @@ class DockObserver extends UEventObserver {
    }

    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 {
        if (mNightMode != mode) {
            mNightMode = mode;
            switch (mode) {
                case MODE_NIGHT_NO:
@@ -316,14 +512,88 @@ class DockObserver extends UEventObserver {
                    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.
                    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;
        }
    }

    /**
     * Wrapper class implementing the IUiModeManager interface.
+103 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }

}