diff options
-rw-r--r-- | InCallUI/AndroidManifest.xml | 1 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/AccelerometerListener.java | 161 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/CallButtonPresenter.java | 6 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/InCallActivity.java | 11 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/InCallPresenter.java | 13 | ||||
-rw-r--r-- | InCallUI/src/com/android/incallui/ProximitySensor.java | 232 |
6 files changed, 424 insertions, 0 deletions
diff --git a/InCallUI/AndroidManifest.xml b/InCallUI/AndroidManifest.xml index 3196dba60..42d8e3173 100644 --- a/InCallUI/AndroidManifest.xml +++ b/InCallUI/AndroidManifest.xml @@ -23,6 +23,7 @@ <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.WAKE_LOCK"/> <application android:name="InCallApp" diff --git a/InCallUI/src/com/android/incallui/AccelerometerListener.java b/InCallUI/src/com/android/incallui/AccelerometerListener.java new file mode 100644 index 000000000..1a7077866 --- /dev/null +++ b/InCallUI/src/com/android/incallui/AccelerometerListener.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2009 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.incallui; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +/** + * This class is used to listen to the accelerometer to monitor the + * orientation of the phone. The client of this class is notified when + * the orientation changes between horizontal and vertical. + */ +public final class AccelerometerListener { + private static final String TAG = "AccelerometerListener"; + private static final boolean DEBUG = true; + private static final boolean VDEBUG = false; + + private SensorManager mSensorManager; + private Sensor mSensor; + + // mOrientation is the orientation value most recently reported to the client. + private int mOrientation; + + // mPendingOrientation is the latest orientation computed based on the sensor value. + // This is sent to the client after a rebounce delay, at which point it is copied to + // mOrientation. + private int mPendingOrientation; + + private OrientationListener mListener; + + // Device orientation + public static final int ORIENTATION_UNKNOWN = 0; + public static final int ORIENTATION_VERTICAL = 1; + public static final int ORIENTATION_HORIZONTAL = 2; + + private static final int ORIENTATION_CHANGED = 1234; + + private static final int VERTICAL_DEBOUNCE = 100; + private static final int HORIZONTAL_DEBOUNCE = 500; + private static final double VERTICAL_ANGLE = 50.0; + + public interface OrientationListener { + public void orientationChanged(int orientation); + } + + public AccelerometerListener(Context context, OrientationListener listener) { + mListener = listener; + mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + + public void enable(boolean enable) { + if (DEBUG) Log.d(TAG, "enable(" + enable + ")"); + synchronized (this) { + if (enable) { + mOrientation = ORIENTATION_UNKNOWN; + mPendingOrientation = ORIENTATION_UNKNOWN; + mSensorManager.registerListener(mSensorListener, mSensor, + SensorManager.SENSOR_DELAY_NORMAL); + } else { + mSensorManager.unregisterListener(mSensorListener); + mHandler.removeMessages(ORIENTATION_CHANGED); + } + } + } + + private void setOrientation(int orientation) { + synchronized (this) { + if (mPendingOrientation == orientation) { + // Pending orientation has not changed, so do nothing. + return; + } + + // Cancel any pending messages. + // We will either start a new timer or cancel alltogether + // if the orientation has not changed. + mHandler.removeMessages(ORIENTATION_CHANGED); + + if (mOrientation != orientation) { + // Set timer to send an event if the orientation has changed since its + // previously reported value. + mPendingOrientation = orientation; + final Message m = mHandler.obtainMessage(ORIENTATION_CHANGED); + // set delay to our debounce timeout + int delay = (orientation == ORIENTATION_VERTICAL ? VERTICAL_DEBOUNCE + : HORIZONTAL_DEBOUNCE); + mHandler.sendMessageDelayed(m, delay); + } else { + // no message is pending + mPendingOrientation = ORIENTATION_UNKNOWN; + } + } + } + + private void onSensorEvent(double x, double y, double z) { + if (VDEBUG) Log.d(TAG, "onSensorEvent(" + x + ", " + y + ", " + z + ")"); + + // If some values are exactly zero, then likely the sensor is not powered up yet. + // ignore these events to avoid false horizontal positives. + if (x == 0.0 || y == 0.0 || z == 0.0) return; + + // magnitude of the acceleration vector projected onto XY plane + final double xy = Math.sqrt(x*x + y*y); + // compute the vertical angle + double angle = Math.atan2(xy, z); + // convert to degrees + angle = angle * 180.0 / Math.PI; + final int orientation = (angle > VERTICAL_ANGLE ? ORIENTATION_VERTICAL : ORIENTATION_HORIZONTAL); + if (VDEBUG) Log.d(TAG, "angle: " + angle + " orientation: " + orientation); + setOrientation(orientation); + } + + SensorEventListener mSensorListener = new SensorEventListener() { + public void onSensorChanged(SensorEvent event) { + onSensorEvent(event.values[0], event.values[1], event.values[2]); + } + + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // ignore + } + }; + + Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case ORIENTATION_CHANGED: + synchronized (this) { + mOrientation = mPendingOrientation; + if (DEBUG) { + Log.d(TAG, "orientation: " + + (mOrientation == ORIENTATION_HORIZONTAL ? "horizontal" + : (mOrientation == ORIENTATION_VERTICAL ? "vertical" + : "unknown"))); + } + mListener.orientationChanged(mOrientation); + } + break; + } + } + }; +} diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java index 12cc65663..233231754 100644 --- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java +++ b/InCallUI/src/com/android/incallui/CallButtonPresenter.java @@ -33,6 +33,7 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto private Call mCall; private AudioModeProvider mAudioModeProvider; + private ProximitySensor mProximitySensor; public CallButtonPresenter() { } @@ -166,6 +167,7 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto public void showDialpadClicked(boolean checked) { Logger.v(this, "Show dialpad " + String.valueOf(checked)); getUi().displayDialpad(checked); + mProximitySensor.onDialpadVisible(checked); } private void updateUi(InCallState state, Call call) { @@ -204,6 +206,10 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto mAudioModeProvider.addListener(this); } + public void setProximitySensor(ProximitySensor proximitySensor) { + mProximitySensor = proximitySensor; + } + public interface CallButtonUi extends Ui { void setVisible(boolean on); void setMute(boolean on); diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java index 5edfaa32a..776f7e013 100644 --- a/InCallUI/src/com/android/incallui/InCallActivity.java +++ b/InCallUI/src/com/android/incallui/InCallActivity.java @@ -19,6 +19,7 @@ package com.android.incallui; import android.app.Activity; import android.app.Fragment; import android.content.Intent; +import android.content.res.Configuration; import android.os.Bundle; import android.view.KeyEvent; import android.view.View; @@ -51,6 +52,9 @@ public class InCallActivity extends Activity { requestWindowFeature(Window.FEATURE_NO_TITLE); + // TODO(klp): Do we need to add this back when prox sensor is not available? + // lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; + // Inflate everything in incall_screen.xml and add it to the screen. setContentView(R.layout.incall_screen); @@ -210,6 +214,11 @@ public class InCallActivity extends Activity { return super.onKeyDown(keyCode, event); } + @Override + public void onConfigurationChanged(Configuration config) { + InCallPresenter.getInstance().getProximitySensor().onConfigurationChanged(config); + } + private void internalResolveIntent(Intent intent) { final String action = intent.getAction(); @@ -282,6 +291,8 @@ public class InCallActivity extends Activity { mCallButtonFragment.getPresenter().setAudioModeProvider( mainPresenter.getAudioModeProvider()); + mCallButtonFragment.getPresenter().setProximitySensor( + mainPresenter.getProximitySensor()); mCallCardFragment.getPresenter().setAudioModeProvider( mainPresenter.getAudioModeProvider()); mCallCardFragment.getPresenter().setContactInfoCache( diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java index 3b400ef7b..1931c1b53 100644 --- a/InCallUI/src/com/android/incallui/InCallPresenter.java +++ b/InCallUI/src/com/android/incallui/InCallPresenter.java @@ -49,6 +49,7 @@ public class InCallPresenter implements CallList.Listener { private InCallActivity mInCallActivity; private boolean mServiceConnected = false; private InCallState mInCallState = InCallState.HIDDEN; + private ProximitySensor mProximitySensor; public static synchronized InCallPresenter getInstance() { if (sInCallPresenter == null) { @@ -74,6 +75,9 @@ public class InCallPresenter implements CallList.Listener { // This only gets called by the service so this is okay. mServiceConnected = true; + mProximitySensor = new ProximitySensor(context, mAudioModeProvider); + addListener(mProximitySensor); + Logger.d(this, "Finished InCallPresenter.setUp"); } @@ -169,6 +173,10 @@ public class InCallPresenter implements CallList.Listener { return mContactInfoCache; } + public ProximitySensor getProximitySensor() { + return mProximitySensor; + } + /** * Hangs up any active or outgoing calls. */ @@ -200,6 +208,10 @@ public class InCallPresenter implements CallList.Listener { if (mStatusBarNotifier != null) { mStatusBarNotifier.updateNotification(mInCallState, mCallList); } + + if (mProximitySensor != null) { + mProximitySensor.onInCallShowing(showing); + } } /** @@ -286,6 +298,7 @@ public class InCallPresenter implements CallList.Listener { private void attemptCleanup() { if (mInCallActivity == null && !mServiceConnected) { Logger.d(this, "Start InCallPresenter.CleanUp"); + mProximitySensor = null; mAudioModeProvider = null; removeListener(mStatusBarNotifier); diff --git a/InCallUI/src/com/android/incallui/ProximitySensor.java b/InCallUI/src/com/android/incallui/ProximitySensor.java new file mode 100644 index 000000000..48dc43c2f --- /dev/null +++ b/InCallUI/src/com/android/incallui/ProximitySensor.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2013 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.incallui; + +import android.content.Context; +import android.content.res.Configuration; +import android.os.PowerManager; + +import com.android.incallui.AudioModeProvider.AudioModeListener; +import com.android.incallui.InCallPresenter.InCallState; +import com.android.incallui.InCallPresenter.InCallStateListener; +import com.android.services.telephony.common.AudioMode; + +/** + * Class manages the proximity sensor for the in-call UI. + * We enable the proximity sensor while the user in a phone call. The Proximity sensor turns off + * the touchscreen and display when the user is close to the screen to prevent user's cheek from + * causing touch events. + * The class requires special knowledge of the activity and device state to know when the proximity + * sensor should be enabled and disabled. Most of that state is fed into this class through + * public methods. + */ +public class ProximitySensor implements AccelerometerListener.OrientationListener, + InCallStateListener, AudioModeListener { + private static final String TAG = ProximitySensor.class.getSimpleName(); + + private final PowerManager mPowerManager; + private final PowerManager.WakeLock mProximityWakeLock; + private final AudioModeProvider mAudioModeProvider; + private final AccelerometerListener mAccelerometerListener; + private int mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN; + private boolean mUiShowing = false; + private boolean mIsPhoneOffhook = false; + private boolean mDialpadVisible; + + // True if the keyboard is currently *not* hidden + // Gets updated whenever there is a Configuration change + private boolean mIsHardKeyboardOpen; + + public ProximitySensor(Context context, AudioModeProvider audioModeProvider) { + mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + + if (mPowerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { + mProximityWakeLock = mPowerManager.newWakeLock( + PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); + } else { + mProximityWakeLock = null; + } + Logger.d(this, "onCreate: mProximityWakeLock: ", mProximityWakeLock); + + mAccelerometerListener = new AccelerometerListener(context, this); + mAudioModeProvider = audioModeProvider; + mAudioModeProvider.addListener(this); + } + + /** + * Called to identify when the device is laid down flat. + */ + @Override + public void orientationChanged(int orientation) { + mOrientation = orientation; + updateProximitySensorMode(); + } + + /** + * Called to keep track of the overall UI state. + */ + @Override + public void onStateChange(InCallState state, CallList callList) { + // We ignore incoming state because we do not want to enable proximity + // sensor during incoming call screen + mIsPhoneOffhook = (InCallState.INCALL == state + || InCallState.OUTGOING == state); + + mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN; + mAccelerometerListener.enable(mIsPhoneOffhook); + + updateProximitySensorMode(); + } + + @Override + public void onSupportedAudioMode(int modeMask) { + } + + /** + * Called when the audio mode changes during a call. + */ + @Override + public void onAudioMode(int mode) { + updateProximitySensorMode(); + } + + public void onDialpadVisible(boolean visible) { + mDialpadVisible = visible; + updateProximitySensorMode(); + } + + /** + * Called by InCallActivity to listen for hard keyboard events. + */ + public void onConfigurationChanged(Configuration newConfig) { + mIsHardKeyboardOpen = newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO; + + // Update the Proximity sensor based on keyboard state + updateProximitySensorMode(); + } + + /** + * Used to save when the UI goes in and out of the foreground. + */ + public void onInCallShowing(boolean showing) { + if (showing) { + mUiShowing = true; + + // We only consider the UI not showing for instances where another app took the foreground. + // If we stopped showing because the screen is off, we still consider that showing. + } else if (mPowerManager.isScreenOn()) { + mUiShowing = false; + } + updateProximitySensorMode(); + } + + /** + * @return true if this device supports the "proximity sensor + * auto-lock" feature while in-call (see updateProximitySensorMode()). + */ + private boolean proximitySensorModeEnabled() { + // TODO(klp): Do we disable notification's expanded view when app is in foreground and + // proximity sensor is on? Is it even possible to do this any more? + return (mProximityWakeLock != null); + } + + /** + * Updates the wake lock used to control proximity sensor behavior, + * based on the current state of the phone. + * + * On devices that have a proximity sensor, to avoid false touches + * during a call, we hold a PROXIMITY_SCREEN_OFF_WAKE_LOCK wake lock + * whenever the phone is off hook. (When held, that wake lock causes + * the screen to turn off automatically when the sensor detects an + * object close to the screen.) + * + * This method is a no-op for devices that don't have a proximity + * sensor. + * + * Proximity wake lock will *not* be held if any one of the + * conditions is true while on a call: + * 1) If the audio is routed via Bluetooth + * 2) If a wired headset is connected + * 3) if the speaker is ON + * 4) If the slider is open(i.e. the hardkeyboard is *not* hidden) + */ + private void updateProximitySensorMode() { + Logger.v(this, "updateProximitySensorMode"); + + if (proximitySensorModeEnabled()) { + Logger.v(this, "keyboard open: ", mIsHardKeyboardOpen); + Logger.v(this, "dialpad visible: ", mDialpadVisible); + Logger.v(this, "isOffhook: ", mIsPhoneOffhook); + + synchronized (mProximityWakeLock) { + + final int audioMode = mAudioModeProvider.getAudioMode(); + Logger.v(this, "audioMode: ", AudioMode.toString(audioMode)); + + // turn proximity sensor off and turn screen on immediately if + // we are using a headset, the keyboard is open, or the device + // is being held in a horizontal position. + boolean screenOnImmediately = (AudioMode.WIRED_HEADSET == audioMode + || AudioMode.SPEAKER == audioMode + || AudioMode.BLUETOOTH == audioMode + || mIsHardKeyboardOpen); + + // We do not keep the screen off when the user is outside in-call screen and we are + // horizontal, but we do not force it on when we become horizontal until the + // proximity sensor goes negative. + final boolean horizontal = + (mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL); + Logger.v(this, "horizontal: ", horizontal); + screenOnImmediately |= !mUiShowing && horizontal; + + // We do not keep the screen off when dialpad is visible, we are horizontal, and + // the in-call screen is being shown. + // At that moment we're pretty sure users want to use it, instead of letting the + // proximity sensor turn off the screen by their hands. + screenOnImmediately |= mDialpadVisible && horizontal; + + Logger.v(this, "screenonImmediately: ", screenOnImmediately); + + if (mIsPhoneOffhook && !screenOnImmediately) { + // Phone is in use! Arrange for the screen to turn off + // automatically when the sensor detects a close object. + if (!mProximityWakeLock.isHeld()) { + Logger.d(this, "updateProximitySensorMode: acquiring..."); + mProximityWakeLock.acquire(); + } else { + Logger.v(this, "updateProximitySensorMode: lock already held."); + } + } else { + // Phone is either idle, or ringing. We don't want any + // special proximity sensor behavior in either case. + if (mProximityWakeLock.isHeld()) { + Logger.d(this, "updateProximitySensorMode: releasing..."); + // Wait until user has moved the phone away from his head if we are + // releasing due to the phone call ending. + // Qtherwise, turn screen on immediately + int flags = + (screenOnImmediately ? 0 : PowerManager.WAIT_FOR_PROXIMITY_NEGATIVE); + mProximityWakeLock.release(flags); + } else { + Logger.v(this, "updateProximitySensorMode: lock already released."); + } + } + } + } + } + +} |