Merge "Blank the screen while playing voicemail using proximity sensor."
diff --git a/res/layout/call_detail.xml b/res/layout/call_detail.xml
index df1ec62..c69f89f 100644
--- a/res/layout/call_detail.xml
+++ b/res/layout/call_detail.xml
@@ -196,4 +196,18 @@
</LinearLayout>
</FrameLayout>
</RelativeLayout>
+
+ <!--
+ Used to hide the UI when playing a voicemail and the proximity sensor
+ is detecting something near the screen.
+ -->
+ <View
+ android:id="@+id/blank"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:background="#000000"
+ android:visibility="gone"
+ />
</RelativeLayout>
diff --git a/src/com/android/contacts/CallDetailActivity.java b/src/com/android/contacts/CallDetailActivity.java
index 6ab4b68..0fadbfb 100644
--- a/src/com/android/contacts/CallDetailActivity.java
+++ b/src/com/android/contacts/CallDetailActivity.java
@@ -69,9 +69,14 @@
* This activity can be either started with the URI of a single call log entry, or with the
* {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries.
*/
-public class CallDetailActivity extends Activity {
+public class CallDetailActivity extends Activity implements ProximitySensorAware {
private static final String TAG = "CallDetail";
+ /** The time to wait before enabling the blank the screen due to the proximity sensor. */
+ private static final long PROXIMITY_BLANK_DELAY_MILLIS = 100;
+ /** The time to wait before disabling the blank the screen due to the proximity sensor. */
+ private static final long PROXIMITY_UNBLANK_DELAY_MILLIS = 500;
+
/** The enumeration of {@link AsyncTask} objects used in this class. */
public enum Tasks {
MARK_VOICEMAIL_READ,
@@ -115,6 +120,60 @@
/** Whether we should show "edit number before call" in the options menu. */
private boolean mHasEditNumberBeforeCall;
+ private ProximitySensorManager mProximitySensorManager;
+ private final ProximitySensorListener mProximitySensorListener = new ProximitySensorListener();
+
+ /** Listener to changes in the proximity sensor state. */
+ private class ProximitySensorListener implements ProximitySensorManager.Listener {
+ /** Used to show a blank view and hide the action bar. */
+ private final Runnable mBlankRunnable = new Runnable() {
+ @Override
+ public void run() {
+ View blankView = findViewById(R.id.blank);
+ blankView.setVisibility(View.VISIBLE);
+ getActionBar().hide();
+ }
+ };
+ /** Used to remove the blank view and show the action bar. */
+ private final Runnable mUnblankRunnable = new Runnable() {
+ @Override
+ public void run() {
+ View blankView = findViewById(R.id.blank);
+ blankView.setVisibility(View.GONE);
+ getActionBar().show();
+ }
+ };
+
+ @Override
+ public synchronized void onNear() {
+ clearPendingRequests();
+ postDelayed(mBlankRunnable, PROXIMITY_BLANK_DELAY_MILLIS);
+ }
+
+ @Override
+ public synchronized void onFar() {
+ clearPendingRequests();
+ postDelayed(mUnblankRunnable, PROXIMITY_UNBLANK_DELAY_MILLIS);
+ }
+
+ /** Removed any delayed requests that may be pending. */
+ public synchronized void clearPendingRequests() {
+ View blankView = findViewById(R.id.blank);
+ blankView.removeCallbacks(mBlankRunnable);
+ blankView.removeCallbacks(mUnblankRunnable);
+ }
+
+ /** Post a {@link Runnable} with a delay on the main thread. */
+ private synchronized void postDelayed(Runnable runnable, long delayMillis) {
+ // Post these instead of executing immediately so that:
+ // - They are guaranteed to be executed on the main thread.
+ // - If the sensor values changes rapidly for some time, the UI will not be
+ // updated immediately.
+ View blankView = findViewById(R.id.blank);
+ blankView.postDelayed(runnable, delayMillis);
+ }
+ }
+
static final String[] CALL_LOG_PROJECTION = new String[] {
CallLog.Calls.DATE,
CallLog.Calls.DURATION,
@@ -189,6 +248,7 @@
mContactBackgroundView = (ImageView) findViewById(R.id.contact_background);
mDefaultCountryIso = ContactsUtils.getCurrentCountryIso(this);
mContactPhotoManager = ContactPhotoManager.getInstance(this);
+ mProximitySensorManager = new ProximitySensorManager(this, mProximitySensorListener);
configureActionBar();
optionallyHandleVoicemail();
}
@@ -770,4 +830,22 @@
startActivity(intent);
finish();
}
+
+ @Override
+ protected void onPause() {
+ // Immediately stop the proximity sensor.
+ disableProximitySensor(false);
+ mProximitySensorListener.clearPendingRequests();
+ super.onPause();
+ }
+
+ @Override
+ public void enableProximitySensor() {
+ mProximitySensorManager.enable();
+ }
+
+ @Override
+ public void disableProximitySensor(boolean waitForFarState) {
+ mProximitySensorManager.disable(waitForFarState);
+ }
}
diff --git a/src/com/android/contacts/ProximitySensorAware.java b/src/com/android/contacts/ProximitySensorAware.java
new file mode 100644
index 0000000..0fb233d
--- /dev/null
+++ b/src/com/android/contacts/ProximitySensorAware.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 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.contacts;
+
+/**
+ * An object that is aware of the state of the proximity sensor.
+ */
+public interface ProximitySensorAware {
+ /** Start tracking the state of the proximity sensor. */
+ public void enableProximitySensor();
+
+ /**
+ * Stop tracking the state of the proximity sensor.
+ *
+ * @param waitForFarState if true and the sensor is currently in the near state, it will wait
+ * until it is again in the far state before stopping to track its state.
+ */
+ public void disableProximitySensor(boolean waitForFarState);
+}
diff --git a/src/com/android/contacts/ProximitySensorManager.java b/src/com/android/contacts/ProximitySensorManager.java
new file mode 100644
index 0000000..69601bf
--- /dev/null
+++ b/src/com/android/contacts/ProximitySensorManager.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2011 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.contacts;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Manages the proximity sensor and notifies a listener when enabled.
+ */
+public class ProximitySensorManager {
+ /**
+ * Listener of the state of the proximity sensor.
+ * <p>
+ * This interface abstracts two possible states for the proximity sensor, near and far.
+ * <p>
+ * The actual meaning of these states depends on the actual sensor.
+ */
+ public interface Listener {
+ /** Called when the proximity sensor transitions from the far to the near state. */
+ public void onNear();
+ /** Called when the proximity sensor transitions from the near to the far state. */
+ public void onFar();
+ }
+
+ public static enum State {
+ NEAR, FAR
+ }
+
+ private final ProximitySensorEventListener mProximitySensorListener;
+
+ /**
+ * The current state of the manager, i.e., whether it is currently tracking the state of the
+ * sensor.
+ */
+ private boolean mManagerEnabled;
+
+ /**
+ * The listener to the state of the sensor.
+ * <p>
+ * Contains most of the logic concerning tracking of the sensor.
+ * <p>
+ * After creating an instance of this object, one should call {@link #register()} and
+ * {@link #unregister()} to enable and disable the notifications.
+ * <p>
+ * Instead of calling unregister, one can call {@link #unregisterWhenFar()} to unregister the
+ * listener the next time the sensor reaches the {@link State#FAR} state if currently in the
+ * {@link State#NEAR} state.
+ */
+ private static class ProximitySensorEventListener implements SensorEventListener {
+ private static final float FAR_THRESHOLD = 5.0f;
+
+ private final SensorManager mSensorManager;
+ private final Sensor mProximitySensor;
+ private final float mMaxValue;
+ private final Listener mListener;
+
+ /**
+ * The last state of the sensor.
+ * <p>
+ * Before registering and after unregistering we are always in the {@link State#FAR} state.
+ */
+ @GuardedBy("this") private State mLastState;
+ /**
+ * If this flag is set to true, we are waiting to reach the {@link State#FAR} state and
+ * should notify the listener and unregister when that happens.
+ */
+ @GuardedBy("this") private boolean mWaitingForFarState;
+
+ public ProximitySensorEventListener(SensorManager sensorManager, Sensor proximitySensor,
+ Listener listener) {
+ mSensorManager = sensorManager;
+ mProximitySensor = proximitySensor;
+ mMaxValue = proximitySensor.getMaximumRange();
+ mListener = listener;
+ // Initialize at far state.
+ mLastState = State.FAR;
+ mWaitingForFarState = false;
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ // Make sure we have a valid value.
+ if (event.values == null) return;
+ if (event.values.length == 0) return;
+ float value = event.values[0];
+ // Convert the sensor into a NEAR/FAR state.
+ State state = getStateFromValue(value);
+ synchronized (this) {
+ // No change in state, do nothing.
+ if (state == mLastState) return;
+ // Keep track of the current state.
+ mLastState = state;
+ // If we are waiting to reach the far state and we are now in it, unregister.
+ if (mWaitingForFarState && mLastState == State.FAR) {
+ unregisterWithoutNotification();
+ }
+ }
+ // Notify the listener of the state change.
+ switch (state) {
+ case NEAR:
+ mListener.onNear();
+ break;
+
+ case FAR:
+ mListener.onFar();
+ break;
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ // Nothing to do here.
+ }
+
+ /** Returns the state of the sensor given its current value. */
+ private State getStateFromValue(float value) {
+ // Determine if the current value corresponds to the NEAR or FAR state.
+ // Take case of the case where the proximity sensor is binary: if the current value is
+ // equal to the maximum, we are always in the FAR state.
+ return (value > FAR_THRESHOLD || value == mMaxValue) ? State.FAR : State.NEAR;
+ }
+
+ /**
+ * Unregister the next time the sensor reaches the {@link State#FAR} state.
+ */
+ public synchronized void unregisterWhenFar() {
+ if (mLastState == State.FAR) {
+ // We are already in the far state, just unregister now.
+ unregisterWithoutNotification();
+ } else {
+ mWaitingForFarState = true;
+ }
+ }
+
+ /** Register the listener and call the listener as necessary. */
+ public synchronized void register() {
+ // It is okay to register multiple times.
+ mSensorManager.registerListener(this, mProximitySensor, SensorManager.SENSOR_DELAY_UI);
+ // We should no longer be waiting for the far state if we are registering again.
+ mWaitingForFarState = false;
+ }
+
+ public void unregister() {
+ State lastState;
+ synchronized (this) {
+ unregisterWithoutNotification();
+ lastState = mLastState;
+ // Always go back to the FAR state. That way, when we register again we will get a
+ // transition when the sensor gets into the NEAR state.
+ mLastState = State.FAR;
+ }
+ // Notify the listener if we changed the state to FAR while unregistering.
+ if (lastState != State.FAR) {
+ mListener.onFar();
+ }
+ }
+
+ @GuardedBy("this")
+ private void unregisterWithoutNotification() {
+ mSensorManager.unregisterListener(this);
+ mWaitingForFarState = false;
+ }
+ }
+
+ public ProximitySensorManager(Context context, Listener listener) {
+ SensorManager sensorManager =
+ (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ Sensor proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ if (proximitySensor == null) {
+ // If there is no sensor, we should not do anything.
+ mProximitySensorListener = null;
+ } else {
+ mProximitySensorListener =
+ new ProximitySensorEventListener(sensorManager, proximitySensor, listener);
+ }
+ }
+
+ /**
+ * Enables the proximity manager.
+ * <p>
+ * The listener will start getting notifications of events.
+ * <p>
+ * This method is idempotent.
+ */
+ public void enable() {
+ if (mProximitySensorListener != null && !mManagerEnabled) {
+ mProximitySensorListener.register();
+ mManagerEnabled = true;
+ }
+ }
+
+ /**
+ * Disables the proximity manager.
+ * <p>
+ * The listener will stop receiving notifications of events, possibly after receiving a last
+ * {@link Listener#onFar()} callback.
+ * <p>
+ * If {@code waitForFarState} is true, if the sensor is not currently in the {@link State#FAR}
+ * state, the listener will receive a {@link Listener#onFar()} callback the next time the sensor
+ * actually reaches the {@link State#FAR} state.
+ * <p>
+ * If {@code waitForFarState} is false, the listener will receive a {@link Listener#onFar()}
+ * callback immediately if the sensor is currently not in the {@link State#FAR} state.
+ * <p>
+ * This method is idempotent.
+ */
+ public void disable(boolean waitForFarState) {
+ if (mProximitySensorListener != null && mManagerEnabled) {
+ if (waitForFarState) {
+ mProximitySensorListener.unregisterWhenFar();
+ } else {
+ mProximitySensorListener.unregister();
+ }
+ mManagerEnabled = false;
+ }
+ }
+}
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
index 7d29406..80f7862 100644
--- a/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackFragment.java
@@ -20,6 +20,7 @@
import static com.android.contacts.CallDetailActivity.EXTRA_VOICEMAIL_URI;
import com.android.common.io.MoreCloseables;
+import com.android.contacts.ProximitySensorAware;
import com.android.contacts.R;
import com.android.contacts.util.AsyncTaskExecutors;
import com.android.ex.variablespeed.MediaPlayerProxy;
@@ -36,6 +37,7 @@
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
+import android.os.PowerManager;
import android.provider.VoicemailContract;
import android.util.Log;
import android.view.LayoutInflater;
@@ -91,10 +93,15 @@
Uri voicemailUri = arguments.getParcelable(EXTRA_VOICEMAIL_URI);
Preconditions.checkNotNull(voicemailUri, "fragment must contain EXTRA_VOICEMAIL_URI");
boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false);
+ PowerManager powerManager =
+ (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
+ PowerManager.WakeLock wakeLock =
+ powerManager.newWakeLock(
+ PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getSimpleName());
mPresenter = new VoicemailPlaybackPresenter(createPlaybackViewImpl(),
createMediaPlayer(mScheduledExecutorService), voicemailUri,
mScheduledExecutorService, startPlayback,
- AsyncTaskExecutors.createAsyncTaskExecutor());
+ AsyncTaskExecutors.createAsyncTaskExecutor(), wakeLock);
mPresenter.onCreate(savedInstanceState);
}
@@ -263,6 +270,24 @@
}
@Override
+ public void enableProximitySensor() {
+ // Only change the state if the activity is still around.
+ Activity activity = mActivityReference.get();
+ if (activity != null && activity instanceof ProximitySensorAware) {
+ ((ProximitySensorAware) activity).enableProximitySensor();
+ }
+ }
+
+ @Override
+ public void disableProximitySensor() {
+ // Only change the state if the activity is still around.
+ Activity activity = mActivityReference.get();
+ if (activity != null && activity instanceof ProximitySensorAware) {
+ ((ProximitySensorAware) activity).disableProximitySensor(true);
+ }
+ }
+
+ @Override
public void registerContentObserver(Uri uri, ContentObserver observer) {
mApplicationContext.getContentResolver().registerContentObserver(uri, false, observer);
}
diff --git a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
index d54cddc..6eb541d 100644
--- a/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/contacts/voicemail/VoicemailPlaybackPresenter.java
@@ -32,6 +32,7 @@
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
+import android.os.PowerManager;
import android.view.View;
import android.widget.SeekBar;
@@ -86,6 +87,8 @@
void setFetchContentTimeout();
void registerContentObserver(Uri uri, ContentObserver observer);
void unregisterContentObserver(ContentObserver observer);
+ void enableProximitySensor();
+ void disableProximitySensor();
}
/** The enumeration of {@link AsyncTask} objects we use in this class. */
@@ -160,16 +163,19 @@
* This variable is thread-contained, accessed only on the ui thread.
*/
private FetchResultHandler mFetchResultHandler;
+ private PowerManager.WakeLock mWakeLock;
public VoicemailPlaybackPresenter(PlaybackView view, MediaPlayerProxy player,
Uri voicemailUri, ScheduledExecutorService executorService,
- boolean startPlayingImmediately, AsyncTaskExecutor asyncTaskExecutor) {
+ boolean startPlayingImmediately, AsyncTaskExecutor asyncTaskExecutor,
+ PowerManager.WakeLock wakeLock) {
mView = view;
mPlayer = player;
mVoicemailUri = voicemailUri;
mStartPlayingImmediately = startPlayingImmediately;
mAsyncTaskExecutor = asyncTaskExecutor;
mPositionUpdater = new PositionUpdater(executorService, SLIDER_UPDATE_PERIOD_MILLIS);
+ mWakeLock = wakeLock;
}
public void onCreate(Bundle bundle) {
@@ -334,6 +340,8 @@
mView.setRateIncreaseButtonListener(createRateIncreaseListener());
mView.setClipPosition(0, mPlayer.getDuration());
mView.playbackStopped();
+ // Always disable on stop.
+ mView.disableProximitySensor();
if (mStartPlayingImmediately) {
resetPrepareStartPlaying(0);
}
@@ -348,16 +356,16 @@
}
}
- /**
- * This method should be called <b>only on the ui thread</b>.
- */
public void onDestroy() {
+ mPlayer.release();
if (mFetchResultHandler != null) {
mFetchResultHandler.destroy();
mFetchResultHandler = null;
}
mPositionUpdater.stopUpdating();
- mPlayer.release();
+ if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
}
private class MediaPlayerErrorListener implements MediaPlayer.OnErrorListener {
@@ -427,6 +435,13 @@
mPlayer.seekTo(startPosition);
mPlayer.start();
mView.playbackStarted();
+ if (!mWakeLock.isHeld()) {
+ mWakeLock.acquire();
+ }
+ // Only enable if we are not currently using the speaker phone.
+ if (!mView.isSpeakerPhoneOn()) {
+ mView.enableProximitySensor();
+ }
mPositionUpdater.startUpdating(startPosition, mDuration.get());
} catch (IOException e) {
handleError(e);
@@ -446,6 +461,11 @@
private void stopPlaybackAtPosition(int clipPosition, int duration) {
mPositionUpdater.stopUpdating();
mView.playbackStopped();
+ if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ // Always disable on stop.
+ mView.disableProximitySensor();
mView.setClipPosition(clipPosition, duration);
if (mPlayer.isPlaying()) {
mPlayer.pause();
@@ -489,7 +509,16 @@
private class SpeakerphoneListener implements View.OnClickListener {
@Override
public void onClick(View v) {
- mView.setSpeakerPhoneOn(!mView.isSpeakerPhoneOn());
+ boolean previousState = mView.isSpeakerPhoneOn();
+ mView.setSpeakerPhoneOn(!previousState);
+ if (mPlayer.isPlaying() && previousState) {
+ // If we are currently playing and we are disabling the speaker phone, enable the
+ // sensor.
+ mView.enableProximitySensor();
+ } else {
+ // If we are not currently playing, disable the sensor.
+ mView.disableProximitySensor();
+ }
}
}
@@ -560,5 +589,8 @@
if (mPlayer.isPlaying()) {
stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
}
+ if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
}
}