Add support for audio device selection in AppRTCDemo.

Summary:

- Creates a list of available (possible to select) audio devices.
- Automatically selects (routes audio) the "best/default" audio device.
- If possible, starts a proximity sensor that will switch between headset earpiece and speaker phone based on how close the a person's ear the mobile device is held.

TBR=glaznev

BUG=4103,4109

Review URL: https://webrtc-codereview.appspot.com/31239004

git-svn-id: http://webrtc.googlecode.com/svn/trunk@7978 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java
index 5f641bc..04c853e 100644
--- a/talk/examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java
@@ -27,32 +27,107 @@
 
 package org.appspot.apprtc;
 
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.media.AudioManager;
 import android.util.Log;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import org.appspot.apprtc.util.AppRTCUtils;
 
 /**
  * AppRTCAudioManager manages all audio related parts of the AppRTC demo.
- * TODO(henrika): add support for device enumeration, device selection etc.
  */
 public class AppRTCAudioManager {
   private static final String TAG = "AppRTCAudioManager";
 
+  // Names of possible audio devices that we currently support.
+  // TODO(henrika): add support for BLUETOOTH as well.
+  public enum AudioDevice {
+    SPEAKER_PHONE,
+    WIRED_HEADSET,
+    EARPIECE,
+  }
+
+  private final Context apprtcContext;
+  private final Runnable onStateChangeListener;
   private boolean initialized = false;
   private AudioManager audioManager;
   private int savedAudioMode = AudioManager.MODE_INVALID;
   private boolean savedIsSpeakerPhoneOn = false;
   private boolean savedIsMicrophoneMute = false;
 
-  /** Construction */
-  static AppRTCAudioManager create(Context context) {
-    return new AppRTCAudioManager(context);
+  // For now; always use the speaker phone as default device selection when
+  // there is a choice between SPEAKER_PHONE and EARPIECE.
+  // TODO(henrika): it is possible that EARPIECE should be preferred in some
+  // cases. If so, we should set this value at construction instead.
+  private final AudioDevice defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
+
+  // Proximity sensor object. It measures the proximity of an object in cm
+  // relative to the view screen of a device and can therefore be used to
+  // assist device switching (close to ear <=> use headset earpiece if
+  // available, far from ear <=> use speaker phone).
+  private AppRTCProximitySensor proximitySensor = null;
+
+  // Contains the currently selected audio device.
+  private AudioDevice selectedAudioDevice;
+
+  // Contains a list of available audio devices. A Set collection is used to
+  // avoid duplicate elements.
+  private final Set<AudioDevice> audioDevices = new HashSet<AudioDevice>();
+
+  // Broadcast receiver for wired headset intent broadcasts.
+  private BroadcastReceiver wiredHeadsetReceiver;
+
+  // This method is called when the proximity sensor reports a state change,
+  // e.g. from "NEAR to FAR" or from "FAR to NEAR".
+  private void onProximitySensorChangedState() {
+    // The proximity sensor should only be activated when there are exactly two
+    // available audio devices.
+    if (audioDevices.size() == 2 &&
+        audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE) &&
+        audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
+      if (proximitySensor.sensorReportsNearState()) {
+        // Sensor reports that a "handset is being held up to a person's ear",
+        // or "something is covering the light sensor".
+        setAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
+      } else {
+        // Sensor reports that a "handset is removed from a person's ear", or
+        // "the light sensor is no longer covered".
+        setAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
+      }
+    }
   }
 
-  private AppRTCAudioManager(Context context) {
-    Log.d(TAG, "AppRTCAudioManager");
+  /** Construction */
+  static AppRTCAudioManager create(Context context,
+      Runnable deviceStateChangeListener) {
+    return new AppRTCAudioManager(context, deviceStateChangeListener);
+  }
+
+  private AppRTCAudioManager(Context context,
+      Runnable deviceStateChangeListener) {
+    apprtcContext = context;
+    onStateChangeListener = deviceStateChangeListener;
     audioManager = ((AudioManager) context.getSystemService(
         Context.AUDIO_SERVICE));
+
+    // Create and initialize the proximity sensor.
+    // Tablet devices (e.g. Nexus 7) does not support proximity sensors.
+    // Note that, the sensor will not be active until start() has been called.
+    proximitySensor = AppRTCProximitySensor.create(context, new Runnable() {
+      // This method will be called each time a state change is detected.
+      // Example: user holds his hand over the device (closer than ~5 cm),
+      // or removes his hand from the device.
+      public void run() {
+        onProximitySensorChangedState();
+      }
+    });
+    AppRTCUtils.logDeviceInfo(TAG);
   }
 
   public void init() {
@@ -66,11 +141,27 @@
     savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
     savedIsMicrophoneMute = audioManager.isMicrophoneMute();
 
+    // Request audio focus before making any device switch.
+    audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL,
+        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+
     // The AppRTC demo shall always run in COMMUNICATION mode since it will
     // result in best possible "VoIP settings", like audio routing, volume
     // control etc.
     audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
 
+    // Always disable microphone mute during a WebRTC call.
+    setMicrophoneMute(false);
+
+    // Do initial selection of audio device. This setting can later be changed
+    // either by adding/removing a wired headset or by covering/uncovering the
+    // proximity sensor.
+    updateAudioDeviceState(hasWiredHeadset());
+
+    // Register receiver for broadcast intents related to adding/removing a
+    // wired headset (Intent.ACTION_HEADSET_PLUG).
+    registerForWiredHeadsetIntentBroadcast();
+
     initialized = true;
   }
 
@@ -80,14 +171,111 @@
       return;
     }
 
+    unregisterForWiredHeadsetIntentBroadcast();
+
     // Restore previously stored audio states.
     setSpeakerphoneOn(savedIsSpeakerPhoneOn);
     setMicrophoneMute(savedIsMicrophoneMute);
     audioManager.setMode(savedAudioMode);
+    audioManager.abandonAudioFocus(null);
+
+    if (proximitySensor != null) {
+      proximitySensor.stop();
+      proximitySensor = null;
+    }
 
     initialized = false;
   }
 
+  /** Changes selection of the currently active audio device. */
+  public void setAudioDevice(AudioDevice device) {
+    Log.d(TAG, "setAudioDevice(device=" + device + ")");
+    AppRTCUtils.assertIsTrue(audioDevices.contains(device));
+
+    switch (device) {
+      case SPEAKER_PHONE:
+        setSpeakerphoneOn(true);
+        selectedAudioDevice = AudioDevice.SPEAKER_PHONE;
+        break;
+      case EARPIECE:
+        setSpeakerphoneOn(false);
+        selectedAudioDevice = AudioDevice.EARPIECE;
+        break;
+      case WIRED_HEADSET:
+        setSpeakerphoneOn(false);
+        selectedAudioDevice = AudioDevice.WIRED_HEADSET;
+        break;
+      default:
+        Log.e(TAG, "Invalid audio device selection");
+        break;
+    }
+    onAudioManagerChangedState();
+  }
+
+  /** Returns current set of available/selectable audio devices. */
+  public Set<AudioDevice> getAudioDevices() {
+    return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices));
+  }
+
+  /** Returns the currently selected audio device. */
+  public AudioDevice getSelectedAudioDevice() {
+    return selectedAudioDevice;
+  }
+
+  /**
+   * Registers receiver for the broadcasted intent when a wired headset is
+   * plugged in or unplugged. The received intent will have an extra
+   * 'state' value where 0 means unplugged, and 1 means plugged.
+   */
+  private void registerForWiredHeadsetIntentBroadcast() {
+    IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+
+    /** Receiver which handles changes in wired headset availability. */
+    wiredHeadsetReceiver = new BroadcastReceiver() {
+      private static final int STATE_UNPLUGGED = 0;
+      private static final int STATE_PLUGGED = 1;
+      private static final int HAS_NO_MIC = 0;
+      private static final int HAS_MIC = 1;
+
+      @Override
+      public void onReceive(Context context, Intent intent) {
+        int state = intent.getIntExtra("state", STATE_UNPLUGGED);
+        int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
+        String name = intent.getStringExtra("name");
+        Log.d(TAG, "BroadcastReceiver.onReceive" + AppRTCUtils.getThreadInfo()
+            + ": "
+            + "a=" + intent.getAction()
+            + ", s=" + (state == STATE_UNPLUGGED ? "unplugged" : "plugged")
+            + ", m=" + (microphone == HAS_MIC ? "mic" : "no mic")
+            + ", n=" + name
+            + ", sb=" + isInitialStickyBroadcast());
+
+        boolean hasWiredHeadset = (state == STATE_PLUGGED) ? true : false;
+        switch (state) {
+          case STATE_UNPLUGGED:
+            updateAudioDeviceState(hasWiredHeadset);
+            break;
+          case STATE_PLUGGED:
+            if (selectedAudioDevice != AudioDevice.WIRED_HEADSET) {
+              updateAudioDeviceState(hasWiredHeadset);
+            }
+            break;
+          default:
+            Log.e(TAG, "Invalid state");
+            break;
+        }
+      }
+    };
+
+    apprtcContext.registerReceiver(wiredHeadsetReceiver, filter);
+  }
+
+  /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */
+  private void unregisterForWiredHeadsetIntentBroadcast() {
+    apprtcContext.unregisterReceiver(wiredHeadsetReceiver);
+    wiredHeadsetReceiver = null;
+  }
+
   /** Sets the speaker phone mode. */
   private void setSpeakerphoneOn(boolean on) {
     boolean wasOn = audioManager.isSpeakerphoneOn();
@@ -105,4 +293,74 @@
     }
     audioManager.setMicrophoneMute(on);
   }
+
+  /** Gets the current earpiece state. */
+  private boolean hasEarpiece() {
+    return apprtcContext.getPackageManager().hasSystemFeature(
+        PackageManager.FEATURE_TELEPHONY);
+  }
+
+  /**
+   * Checks whether a wired headset is connected or not.
+   * This is not a valid indication that audio playback is actually over
+   * the wired headset as audio routing depends on other conditions. We
+   * only use it as an early indicator (during initialization) of an attached
+   * wired headset.
+   */
+  @Deprecated
+  private boolean hasWiredHeadset() {
+    return audioManager.isWiredHeadsetOn();
+  }
+
+  /** Update list of possible audio devices and make new device selection. */
+  private void updateAudioDeviceState(boolean hasWiredHeadset) {
+    // Update the list of available audio devices.
+    audioDevices.clear();
+    if (hasWiredHeadset) {
+      // If a wired headset is connected, then it is the only possible option.
+      audioDevices.add(AudioDevice.WIRED_HEADSET);
+    } else {
+      // No wired headset, hence the audio-device list can contain speaker
+      // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
+      audioDevices.add(AudioDevice.SPEAKER_PHONE);
+      if (hasEarpiece())  {
+        audioDevices.add(AudioDevice.EARPIECE);
+      }
+    }
+    Log.d(TAG, "audioDevices: " + audioDevices);
+
+    // Switch to correct audio device given the list of available audio devices.
+    if (hasWiredHeadset) {
+      setAudioDevice(AudioDevice.WIRED_HEADSET);
+    } else {
+      setAudioDevice(defaultAudioDevice);
+    }
+  }
+
+  /** Called each time a new audio device has been added or removed. */
+  private void onAudioManagerChangedState() {
+    Log.d(TAG, "onAudioManagerChangedState: devices=" + audioDevices
+        + ", selected=" + selectedAudioDevice);
+
+    // Enable the proximity sensor if there are two available audio devices
+    // in the list. Given the current implementation, we know that the choice
+    // will then be between EARPIECE and SPEAKER_PHONE.
+    if (audioDevices.size() == 2) {
+      AppRTCUtils.assertIsTrue(audioDevices.contains(AudioDevice.EARPIECE) &&
+          audioDevices.contains(AudioDevice.SPEAKER_PHONE));
+      // Start the proximity sensor.
+      proximitySensor.start();
+    } else if (audioDevices.size() == 1) {
+      // Stop the proximity sensor since it is no longer needed.
+      proximitySensor.stop();
+    } else {
+      Log.e(TAG, "Invalid device list");
+    }
+
+    if (onStateChangeListener != null) {
+      // Run callback to notify a listening client. The client can then
+      // use public getters to query the new state.
+      onStateChangeListener.run();
+    }
+  }
 }
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
index 72adf89..bc8c629 100644
--- a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
@@ -203,7 +203,14 @@
 
     // Create and audio manager that will take care of audio routing,
     // audio modes, audio device enumeration etc.
-    audioManager = AppRTCAudioManager.create(this);
+    audioManager = AppRTCAudioManager.create(this, new Runnable() {
+        // This method will be called each time the audio state (number and
+        // type of devices) has been changed.
+        public void run() {
+          onAudioManagerChangedState();
+        }
+      }
+    );
 
     final Intent intent = getIntent();
     Uri url = intent.getData();
@@ -294,6 +301,11 @@
     }
   }
 
+  private void onAudioManagerChangedState() {
+    // TODO(henrika): disable video if AppRTCAudioManager.AudioDevice.EARPIECE
+    // is active.
+  }
+
   // Disconnect from remote resources, dispose of local resources, and exit.
   private void disconnect() {
     if (appRtcClient != null) {
@@ -595,5 +607,4 @@
       disconnectWithErrorMessage(description);
     }
   }
-
 }
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCProximitySensor.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCProximitySensor.java
new file mode 100644
index 0000000..76f5d8f
--- /dev/null
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCProximitySensor.java
@@ -0,0 +1,187 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.appspot.apprtc;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.util.Log;
+import java.util.List;
+import org.appspot.apprtc.util.AppRTCUtils;
+import org.appspot.apprtc.util.AppRTCUtils.NonThreadSafe;
+
+/**
+ * AppRTCProximitySensor manages functions related to the proximity sensor in
+ * the AppRTC demo.
+ * On most device, the proximity sensor is implemented as a boolean-sensor.
+ * It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX
+ * value i.e. the LUX value of the light sensor is compared with a threshold.
+ * A LUX-value more than the threshold means the proximity sensor returns "FAR".
+ * Anything less than the threshold value and the sensor  returns "NEAR".
+ */
+public class AppRTCProximitySensor implements SensorEventListener {
+  private static final String TAG = "AppRTCProximitySensor";
+
+  // This class should be created, started and stopped on one thread
+  // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is
+  // the case. Only active when |DEBUG| is set to true.
+  private final NonThreadSafe nonThreadSafe = new AppRTCUtils.NonThreadSafe();
+
+  private final Context apprtcContext;
+  private final Runnable onSensorStateListener;
+  private final SensorManager sensorManager;
+  private Sensor proximitySensor = null;
+  private boolean initialized = false;
+  private boolean lastStateReportIsNear = false;
+
+  /** Construction */
+  static AppRTCProximitySensor create(Context context,
+      Runnable sensorStateListener) {
+    return new AppRTCProximitySensor(context, sensorStateListener);
+  }
+
+  private AppRTCProximitySensor(Context context, Runnable sensorStateListener) {
+    Log.d(TAG, "AppRTCProximitySensor" + AppRTCUtils.getThreadInfo());
+    apprtcContext = context;
+    onSensorStateListener = sensorStateListener;
+    sensorManager = ((SensorManager) context.getSystemService(
+        Context.SENSOR_SERVICE));
+  }
+
+  /**
+   * Activate the proximity sensor. Also do initializtion if called for the
+   * first time.
+   */
+  public boolean start() {
+    checkIfCalledOnValidThread();
+    Log.d(TAG, "start" + AppRTCUtils.getThreadInfo());
+    if (!initDefaultSensor()) {
+      // Proximity sensor is not supported on this device.
+      return false;
+    }
+    sensorManager.registerListener(
+        this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
+    return true;
+  }
+
+  /** Deactivate the proximity sensor. */
+  public void stop() {
+    checkIfCalledOnValidThread();
+    Log.d(TAG, "stop" + AppRTCUtils.getThreadInfo());
+    if (proximitySensor == null) {
+      return;
+    }
+    sensorManager.unregisterListener(this, proximitySensor);
+  }
+
+  /** Getter for last reported state. Set to true if "near" is reported. */
+  public boolean sensorReportsNearState() {
+    checkIfCalledOnValidThread();
+    return lastStateReportIsNear;
+  }
+
+  @Override
+  public final void onAccuracyChanged(Sensor sensor, int accuracy) {
+    checkIfCalledOnValidThread();
+    AppRTCUtils.assertIsTrue(sensor.getType() == Sensor.TYPE_PROXIMITY);
+    if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
+      Log.e(TAG, "The values returned by this sensor cannot be trusted");
+    }
+  }
+
+  @Override
+  public final void onSensorChanged(SensorEvent event) {
+    checkIfCalledOnValidThread();
+    AppRTCUtils.assertIsTrue(event.sensor.getType() == Sensor.TYPE_PROXIMITY);
+    // As a best practice; do as little as possible within this method and
+    // avoid blocking.
+    float distanceInCentimeters = event.values[0];
+    if (distanceInCentimeters < proximitySensor.getMaximumRange()) {
+      Log.d(TAG, "Proximity sensor => NEAR state");
+      lastStateReportIsNear = true;
+    } else {
+      Log.d(TAG, "Proximity sensor => FAR state");
+      lastStateReportIsNear = false;
+    }
+
+    // Report about new state to listening client. Client can then call
+    // sensorReportsNearState() to query the current state (NEAR or FAR).
+    if (onSensorStateListener != null) {
+      onSensorStateListener.run();
+    }
+
+    Log.d(TAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": "
+        + "accuracy=" + event.accuracy
+        + ", timestamp=" + event.timestamp + ", distance=" + event.values[0]);
+  }
+
+  /**
+   * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7)
+   * does not support this type of sensor and false will be retured in such
+   * cases.
+   */
+  private boolean initDefaultSensor() {
+    if (proximitySensor != null) {
+      return true;
+    }
+    proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+    if (proximitySensor == null) {
+      return false;
+    }
+    logProximitySensorInfo();
+    return true;
+  }
+
+  /** Helper method for logging information about the proximity sensor. */
+  private void logProximitySensorInfo() {
+    if (proximitySensor == null)
+      return;
+    Log.d(TAG, "Proximity sensor: " + "name=" + proximitySensor.getName()
+        + ", vendor: " + proximitySensor.getVendor()
+        + ", type: " + proximitySensor.getStringType()
+        + ", reporting mode: " + proximitySensor.getReportingMode()
+        + ", power: " + proximitySensor.getPower()
+        + ", min delay: " + proximitySensor.getMinDelay()
+        + ", max delay: " + proximitySensor.getMaxDelay()
+        + ", resolution: " + proximitySensor.getResolution()
+        + ", max range: " + proximitySensor.getMaximumRange()
+        + ", isWakeUpSensor: " + proximitySensor.isWakeUpSensor());
+  }
+
+  /**
+   * Helper method for debugging purposes. Ensures that method is
+   * called on same thread as this object was created on.
+   */
+  private void checkIfCalledOnValidThread() {
+    if (!nonThreadSafe.calledOnValidThread()) {
+      throw new IllegalStateException("Method is not called on valid thread");
+    }
+  }
+}
diff --git a/talk/examples/android/src/org/appspot/apprtc/util/AppRTCUtils.java b/talk/examples/android/src/org/appspot/apprtc/util/AppRTCUtils.java
new file mode 100644
index 0000000..51896ba
--- /dev/null
+++ b/talk/examples/android/src/org/appspot/apprtc/util/AppRTCUtils.java
@@ -0,0 +1,82 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  1. Redistributions of source code must retain the above copyright notice,
+ *     this list of conditions and the following disclaimer.
+ *  2. Redistributions in binary form must reproduce the above copyright notice,
+ *     this list of conditions and the following disclaimer in the documentation
+ *     and/or other materials provided with the distribution.
+ *  3. The name of the author may not be used to endorse or promote products
+ *     derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.appspot.apprtc.util;
+
+import android.os.Build;
+import android.util.Log;
+import java.lang.Thread;
+
+public final class AppRTCUtils {
+
+  private AppRTCUtils() {
+  }
+
+  /**
+   * NonThreadSafe is a helper class used to help verify that methods of a
+   * class are called from the same thread.
+   */
+  public static class NonThreadSafe {
+    private final Long threadId;
+
+    public NonThreadSafe() {
+      // Store thread ID of the creating thread.
+      threadId = Thread.currentThread().getId();
+    }
+
+   /** Checks if the method is called on the valid/creating thread. */
+    public boolean calledOnValidThread() {
+       return threadId.equals(Thread.currentThread().getId());
+    }
+  }
+
+  /** Helper method which throws an exception  when an assertion has failed. */
+  public static void assertIsTrue(boolean condition) {
+    if (!condition) {
+      throw new AssertionError("Expected condition to be true");
+    }
+  }
+
+  /** Helper method for building a string of thread information.*/
+  public static String getThreadInfo() {
+    return "@[name=" + Thread.currentThread().getName()
+        + ", id=" + Thread.currentThread().getId() + "]";
+  }
+
+  /** Information about the current build, taken from system properties. */
+  public static void logDeviceInfo(String tag) {
+    Log.d(tag, "Android SDK: " + Build.VERSION.SDK_INT + ", "
+        + "Release: " + Build.VERSION.RELEASE + ", "
+        + "Brand: " + Build.BRAND + ", "
+        + "Device: " + Build.DEVICE + ", "
+        + "Id: " + Build.ID + ", "
+        + "Hardware: " + Build.HARDWARE + ", "
+        + "Manufacturer: " + Build.MANUFACTURER + ", "
+        + "Model: " + Build.MODEL + ", "
+        + "Product: " + Build.PRODUCT);
+  }
+}
diff --git a/talk/libjingle_examples.gyp b/talk/libjingle_examples.gyp
index 63aa5f1..8f806f4 100755
--- a/talk/libjingle_examples.gyp
+++ b/talk/libjingle_examples.gyp
@@ -327,12 +327,14 @@
                 'examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java',
                 'examples/android/src/org/appspot/apprtc/AppRTCClient.java',
                 'examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java',
+                'examples/android/src/org/appspot/apprtc/AppRTCProximitySensor.java',
                 'examples/android/src/org/appspot/apprtc/ConnectActivity.java',
                 'examples/android/src/org/appspot/apprtc/PeerConnectionClient.java',
                 'examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java',
                 'examples/android/src/org/appspot/apprtc/SettingsActivity.java',
                 'examples/android/src/org/appspot/apprtc/SettingsFragment.java',
                 'examples/android/src/org/appspot/apprtc/UnhandledExceptionHandler.java',
+                'examples/android/src/org/appspot/apprtc/util/AppRTCUtils.java',
                 'examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java',
                 'examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java',
               ],