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();
+        }
     }
 }