Merge "Hook up emergency metrics"
diff --git a/res/values/config.xml b/res/values/config.xml
index 8c84688..c0e9669 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -60,4 +60,13 @@
          between repeats of the ringtone.
          When false, the ringtone will be looping with no pause. -->
     <bool name="should_pause_between_ringtone_repeats">true</bool>
+
+    <!-- Threshold for the X+Y component of gravity needed for the device orientation to be
+         classified as being on a user's ear. -->
+    <item name="device_on_ear_xy_gravity_threshold" format="float" type="dimen">5.5</item>
+
+    <!-- Lower threshold for the Y-component of gravity needed for the device orientation to be
+         classified as being on a user's ear. If the Y-component is less than this negative value,
+         the device is probably upside-down and therefore not on a ear -->
+    <item name="device_on_ear_y_gravity_negative_threshold" format="float" type="dimen">-1</item>
 </resources>
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 2ae7b2a..a8f2bc8 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -446,9 +446,9 @@
     }
 
     @VisibleForTesting
-    public void startCallWaiting() {
+    public void startCallWaiting(String reason) {
         if (mRingingCalls.size() == 1) {
-            mRinger.startCallWaiting(mRingingCalls.iterator().next());
+            mRinger.startCallWaiting(mRingingCalls.iterator().next(), reason);
         }
     }
 
diff --git a/src/com/android/server/telecom/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index 92e2973..42a1d36 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -30,8 +30,9 @@
 
 public class CallAudioModeStateMachine extends StateMachine {
     public static class Factory {
-        public CallAudioModeStateMachine create(AudioManager am) {
-            return new CallAudioModeStateMachine(am);
+        public CallAudioModeStateMachine create(SystemStateHelper systemStateHelper,
+                AudioManager am) {
+            return new CallAudioModeStateMachine(systemStateHelper, am);
         }
     }
 
@@ -333,7 +334,7 @@
                     return HANDLED;
                 case NEW_RINGING_CALL:
                     // Don't make a call ring over an active call, but do play a call waiting tone.
-                    mCallAudioManager.startCallWaiting();
+                    mCallAudioManager.startCallWaiting("call already active");
                     return HANDLED;
                 case NEW_HOLDING_CALL:
                     // Don't do anything now. Putting an active call on hold will be handled when
@@ -388,7 +389,7 @@
                     return HANDLED;
                 case NEW_RINGING_CALL:
                     // Don't make a call ring over an active call, but do play a call waiting tone.
-                    mCallAudioManager.startCallWaiting();
+                    mCallAudioManager.startCallWaiting("call already active");
                     return HANDLED;
                 case NEW_HOLDING_CALL:
                     // Don't do anything now. Putting an active call on hold will be handled when
@@ -442,8 +443,14 @@
                             ? mVoipCallFocusState : mSimCallFocusState);
                     return HANDLED;
                 case NEW_RINGING_CALL:
-                    // Apparently this is current behavior. Should this be the case?
-                    transitionTo(mRingingFocusState);
+                    // TODO: consider whether to move this into MessageArgs if more things start
+                    // to use it.
+                    if (args.hasHoldingCalls && mSystemStateHelper.isDeviceAtEar()) {
+                        mCallAudioManager.startCallWaiting(
+                                "Device is at ear with held call");
+                    } else {
+                        transitionTo(mRingingFocusState);
+                    }
                     return HANDLED;
                 case NEW_HOLDING_CALL:
                     // Do nothing.
@@ -470,14 +477,17 @@
     private final BaseState mOtherFocusState = new OtherFocusState();
 
     private final AudioManager mAudioManager;
+    private final SystemStateHelper mSystemStateHelper;
     private CallAudioManager mCallAudioManager;
 
     private int mMostRecentMode;
     private boolean mIsInitialized = false;
 
-    public CallAudioModeStateMachine(AudioManager audioManager) {
+    public CallAudioModeStateMachine(SystemStateHelper systemStateHelper,
+            AudioManager audioManager) {
         super(CallAudioModeStateMachine.class.getSimpleName());
         mAudioManager = audioManager;
+        mSystemStateHelper = systemStateHelper;
         mMostRecentMode = AudioManager.MODE_NORMAL;
 
         addState(mUnfocusedState);
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index f702e1f..941259b 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -360,7 +360,7 @@
             CallAudioManager.AudioServiceFactory audioServiceFactory,
             BluetoothRouteManager bluetoothManager,
             WiredHeadsetManager wiredHeadsetManager,
-            SystemStateProvider systemStateProvider,
+            SystemStateHelper systemStateHelper,
             DefaultDialerCache defaultDialerCache,
             Timeouts.Adapter timeoutsAdapter,
             AsyncRingtonePlayer asyncRingtonePlayer,
@@ -422,7 +422,7 @@
         RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context);
         SystemVibrator systemVibrator = new SystemVibrator(context);
         mInCallController = inCallControllerFactory.create(context, mLock, this,
-                systemStateProvider, defaultDialerCache, mTimeoutsAdapter,
+                systemStateHelper, defaultDialerCache, mTimeoutsAdapter,
                 emergencyCallHelper);
         mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
                 ringtoneFactory, systemVibrator,
@@ -430,8 +430,8 @@
         mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext,
                 (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE), mLock);
         mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine,
-                this, callAudioModeStateMachineFactory.create((AudioManager)
-                        mContext.getSystemService(Context.AUDIO_SERVICE)),
+                this, callAudioModeStateMachineFactory.create(systemStateHelper,
+                (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)),
                 playerFactory, mRinger, new RingbackPlayer(playerFactory),
                 bluetoothStateReceiver, mDtmfLocalTonePlayer);
 
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 6f3c4d5..a413814 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -47,7 +47,7 @@
 // TODO: Needed for move to system service: import com.android.internal.R;
 import com.android.internal.telecom.IInCallService;
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.telecom.SystemStateProvider.SystemStateListener;
+import com.android.server.telecom.SystemStateHelper.SystemStateListener;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -729,7 +729,7 @@
     private final Context mContext;
     private final TelecomSystem.SyncRoot mLock;
     private final CallsManager mCallsManager;
-    private final SystemStateProvider mSystemStateProvider;
+    private final SystemStateHelper mSystemStateHelper;
     private final Timeouts.Adapter mTimeoutsAdapter;
     private final DefaultDialerCache mDefaultDialerCache;
     private final EmergencyCallHelper mEmergencyCallHelper;
@@ -737,13 +737,13 @@
     private NonUIInCallServiceConnectionCollection mNonUIInCallServiceConnections;
 
     public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
-            SystemStateProvider systemStateProvider,
+            SystemStateHelper systemStateHelper,
             DefaultDialerCache defaultDialerCache, Timeouts.Adapter timeoutsAdapter,
             EmergencyCallHelper emergencyCallHelper) {
         mContext = context;
         mLock = lock;
         mCallsManager = callsManager;
-        mSystemStateProvider = systemStateProvider;
+        mSystemStateHelper = systemStateHelper;
         mTimeoutsAdapter = timeoutsAdapter;
         mDefaultDialerCache = defaultDialerCache;
         mEmergencyCallHelper = emergencyCallHelper;
@@ -753,7 +753,7 @@
                 resources.getString(R.string.ui_default_package),
                 resources.getString(R.string.incall_default_class));
 
-        mSystemStateProvider.addListener(mSystemStateListener);
+        mSystemStateHelper.addListener(mSystemStateListener);
     }
 
     @Override
@@ -1227,7 +1227,7 @@
     }
 
     private boolean shouldUseCarModeUI() {
-        return mSystemStateProvider.isCarMode();
+        return mSystemStateHelper.isCarMode();
     }
 
     /**
diff --git a/src/com/android/server/telecom/InCallControllerFactory.java b/src/com/android/server/telecom/InCallControllerFactory.java
index e384af7..c3a7831 100644
--- a/src/com/android/server/telecom/InCallControllerFactory.java
+++ b/src/com/android/server/telecom/InCallControllerFactory.java
@@ -23,6 +23,6 @@
  */
 public interface InCallControllerFactory {
     InCallController create(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
-            SystemStateProvider systemStateProvider, DefaultDialerCache defaultDialerCache,
+            SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache,
             Timeouts.Adapter timeoutsAdapter, EmergencyCallHelper emergencyCallHelper);
 }
diff --git a/src/com/android/server/telecom/QuickResponseUtils.java b/src/com/android/server/telecom/QuickResponseUtils.java
index 5f8269d..84d2636 100644
--- a/src/com/android/server/telecom/QuickResponseUtils.java
+++ b/src/com/android/server/telecom/QuickResponseUtils.java
@@ -117,4 +117,48 @@
         Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - Done.");
         return;
     }
+
+    /**
+     * Determine if the user has changed any of the quick responses back to exactly the same text as
+     * the default text.  If they did, clear the preference so we'll rely on the default value and
+     * still be able to re-translate automatically when language changes occur.
+     *
+     * @param context The current context.
+     * @param prefs   The quick response shared prefs.
+     */
+    public static void maybeResetQuickResponses(Context context, SharedPreferences prefs) {
+        final Resources res = context.getResources();
+
+        String defaultResponse1 = res.getString(R.string.respond_via_sms_canned_response_1);
+        String currentValue1 = prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_1, "");
+        if (currentValue1.equals(defaultResponse1)) {
+            prefs.edit().remove(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_1).apply();
+            Log.i(QuickResponseUtils.class,
+                    "maybeResetQuickResponses: response 1 is identical to default; clear pref.");
+        }
+
+        String defaultResponse2 = res.getString(R.string.respond_via_sms_canned_response_2);
+        String currentValue2 = prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_2, "");
+        if (currentValue2.equals(defaultResponse2)) {
+            prefs.edit().remove(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_2).apply();
+            Log.i(QuickResponseUtils.class,
+                    "maybeResetQuickResponses: response 2 is identical to default; clear pref.");
+        }
+
+        String defaultResponse3 = res.getString(R.string.respond_via_sms_canned_response_3);
+        String currentValue3 = prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_3, "");
+        if (currentValue3.equals(defaultResponse3)) {
+            prefs.edit().remove(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_3).apply();
+            Log.i(QuickResponseUtils.class,
+                    "maybeResetQuickResponses: response 3 is identical to default; clear pref.");
+        }
+
+        String defaultResponse4 = res.getString(R.string.respond_via_sms_canned_response_4);
+        String currentValue4 = prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_4, "");
+        if (currentValue4.equals(defaultResponse4)) {
+            prefs.edit().remove(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_4).apply();
+            Log.i(QuickResponseUtils.class,
+                    "maybeResetQuickResponses: response 4 is identical to default; clear pref.");
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java
index 964f6ad..fadc6b5 100644
--- a/src/com/android/server/telecom/RespondViaSmsManager.java
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -105,6 +105,11 @@
                 final ArrayList<String> textMessages = new ArrayList<>(
                         QuickResponseUtils.NUM_CANNED_RESPONSES);
 
+                // Where the user has changed a quick response back to the same text as the
+                // original text, clear the shared pref.  This ensures we always load the resource
+                // in the current active language.
+                QuickResponseUtils.maybeResetQuickResponses(context, prefs);
+
                 // Note the default values here must agree with the corresponding
                 // android:defaultValue attributes in respond_via_sms_settings.xml.
                 textMessages.add(0, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_1,
diff --git a/src/com/android/server/telecom/RespondViaSmsSettings.java b/src/com/android/server/telecom/RespondViaSmsSettings.java
index 2ea204a..3bee5f7 100644
--- a/src/com/android/server/telecom/RespondViaSmsSettings.java
+++ b/src/com/android/server/telecom/RespondViaSmsSettings.java
@@ -54,6 +54,7 @@
 
         getPreferenceManager().setSharedPreferencesName(QuickResponseUtils.SHARED_PREFERENCES_NAME);
         mPrefs = getPreferenceManager().getSharedPreferences();
+        QuickResponseUtils.maybeResetQuickResponses(this, mPrefs);
     }
 
     @Override
@@ -108,6 +109,9 @@
         SharedPreferences.Editor editor = mPrefs.edit();
         editor.putString(pref.getKey(), (String) newValue).commit();
 
+        // If the user just reset the quick response to its original text, clear the pref.
+        QuickResponseUtils.maybeResetQuickResponses(this, mPrefs);
+
         return true;  // means it's OK to update the state of the Preference with the new value
     }
 
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 51976e1..ee423da 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -233,6 +233,10 @@
     }
 
     public void startCallWaiting(Call call) {
+        startCallWaiting(call, null);
+    }
+
+    public void startCallWaiting(Call call, String reason) {
         if (mSystemSettingsUtil.isTheaterModeOn(mContext)) {
             return;
         }
@@ -252,7 +256,7 @@
         stopRinging();
 
         if (mCallWaitingPlayer == null) {
-            Log.addEvent(call, LogUtils.Events.START_CALL_WAITING_TONE);
+            Log.addEvent(call, LogUtils.Events.START_CALL_WAITING_TONE, reason);
             mCallWaitingCall = call;
             mCallWaitingPlayer =
                     mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
index f15570b..cf5407d 100644
--- a/src/com/android/server/telecom/ServiceBinder.java
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -39,7 +39,7 @@
  * Subclasses supply the service intent and component name and this class will invoke protected
  * methods when the class is bound, unbound, or upon failure.
  */
-abstract class ServiceBinder {
+public abstract class ServiceBinder {
 
     /**
      * Callback to notify after a binding succeeds or fails.
diff --git a/src/com/android/server/telecom/SystemStateHelper.java b/src/com/android/server/telecom/SystemStateHelper.java
new file mode 100644
index 0000000..69a46c6
--- /dev/null
+++ b/src/com/android/server/telecom/SystemStateHelper.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom;
+
+import android.app.UiModeManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.telecom.Log;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Provides various system states to the rest of the telecom codebase.
+ */
+public class SystemStateHelper {
+    public static interface SystemStateListener {
+        public void onCarModeChanged(boolean isCarMode);
+    }
+
+    private final Context mContext;
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.startSession("SSP.oR");
+            try {
+                String action = intent.getAction();
+                if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(action)) {
+                    onEnterCarMode();
+                } else if (UiModeManager.ACTION_EXIT_CAR_MODE.equals(action)) {
+                    onExitCarMode();
+                } else {
+                    Log.w(this, "Unexpected intent received: %s", intent.getAction());
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
+    };
+
+    private Set<SystemStateListener> mListeners = new CopyOnWriteArraySet<>();
+    private boolean mIsCarMode;
+
+    public SystemStateHelper(Context context) {
+        mContext = context;
+
+        IntentFilter intentFilter = new IntentFilter(UiModeManager.ACTION_ENTER_CAR_MODE);
+        intentFilter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE);
+        mContext.registerReceiver(mBroadcastReceiver, intentFilter);
+        Log.i(this, "Registering car mode receiver: %s", intentFilter);
+
+        mIsCarMode = getSystemCarMode();
+    }
+
+    public void addListener(SystemStateListener listener) {
+        if (listener != null) {
+            mListeners.add(listener);
+        }
+    }
+
+    public boolean removeListener(SystemStateListener listener) {
+        return mListeners.remove(listener);
+    }
+
+    public boolean isCarMode() {
+        return mIsCarMode;
+    }
+
+    public boolean isDeviceAtEar() {
+        return isDeviceAtEar(mContext);
+    }
+
+    /**
+     * Returns a guess whether the phone is up to the user's ear. Use the proximity sensor and
+     * the gravity sensor to make a guess
+     * @return true if the proximity sensor is activated, the magnitude of gravity in directions
+     *         parallel to the screen is greater than some configurable threshold, and the
+     *         y-component of gravity isn't less than some other configurable threshold.
+     */
+    public static boolean isDeviceAtEar(Context context) {
+        SensorManager sm = context.getSystemService(SensorManager.class);
+        if (sm == null) {
+            return false;
+        }
+        Sensor grav = sm.getDefaultSensor(Sensor.TYPE_GRAVITY);
+        Sensor proximity = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+        if (grav == null || proximity == null) {
+            return false;
+        }
+
+        AtomicBoolean result = new AtomicBoolean(true);
+        CountDownLatch gravLatch = new CountDownLatch(1);
+        CountDownLatch proxLatch = new CountDownLatch(1);
+
+        final double xyGravityThreshold = context.getResources().getFloat(
+                R.dimen.device_on_ear_xy_gravity_threshold);
+        final double yGravityNegativeThreshold = context.getResources().getFloat(
+                R.dimen.device_on_ear_y_gravity_negative_threshold);
+
+        SensorEventListener listener = new SensorEventListener() {
+            @Override
+            public void onSensorChanged(SensorEvent event) {
+                if (event.sensor.getType() == Sensor.TYPE_GRAVITY) {
+                    if (gravLatch.getCount() == 0) {
+                        return;
+                    }
+                    double xyMag = Math.sqrt(event.values[0] * event.values[0]
+                            + event.values[1] * event.values[1]);
+                    if (xyMag < xyGravityThreshold
+                            || event.values[1] < yGravityNegativeThreshold) {
+                        result.set(false);
+                    }
+                    gravLatch.countDown();
+                } else if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) {
+                    if (proxLatch.getCount() == 0) {
+                        return;
+                    }
+                    if (event.values[0] >= proximity.getMaximumRange()) {
+                        result.set(false);
+                    }
+                    proxLatch.countDown();
+                }
+            }
+
+            @Override
+            public void onAccuracyChanged(Sensor sensor, int accuracy) {
+            }
+        };
+
+        try {
+            sm.registerListener(listener, grav, SensorManager.SENSOR_DELAY_FASTEST);
+            sm.registerListener(listener, proximity, SensorManager.SENSOR_DELAY_FASTEST);
+            boolean accelValid = gravLatch.await(100, TimeUnit.MILLISECONDS);
+            boolean proxValid = proxLatch.await(100, TimeUnit.MILLISECONDS);
+            if (accelValid && proxValid) {
+                return result.get();
+            } else {
+                Log.w(SystemStateHelper.class.getSimpleName(),
+                        "Timed out waiting for sensors: %b %b", accelValid, proxValid);
+                return false;
+            }
+        } catch (InterruptedException e) {
+            return false;
+        } finally {
+            sm.unregisterListener(listener);
+        }
+    }
+
+    private void onEnterCarMode() {
+        if (!mIsCarMode) {
+            Log.i(this, "Entering carmode");
+            mIsCarMode = true;
+            notifyCarMode();
+        }
+    }
+
+    private void onExitCarMode() {
+        if (mIsCarMode) {
+            Log.i(this, "Exiting carmode");
+            mIsCarMode = false;
+            notifyCarMode();
+        }
+    }
+
+    private void notifyCarMode() {
+        for (SystemStateListener listener : mListeners) {
+            listener.onCarModeChanged(mIsCarMode);
+        }
+    }
+
+    /**
+     * Checks the system for the current car mode.
+     *
+     * @return True if in car mode, false otherwise.
+     */
+    private boolean getSystemCarMode() {
+        UiModeManager uiModeManager =
+                (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
+
+        if (uiModeManager != null) {
+            return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
+        }
+
+        return false;
+    }
+}
diff --git a/src/com/android/server/telecom/SystemStateProvider.java b/src/com/android/server/telecom/SystemStateProvider.java
deleted file mode 100644
index e1938b1..0000000
--- a/src/com/android/server/telecom/SystemStateProvider.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.server.telecom;
-
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
-import android.telecom.Log;
-
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-/**
- * Provides various system states to the rest of the telecom codebase. So far, that's only car-mode.
- */
-public class SystemStateProvider {
-
-    public static interface SystemStateListener {
-        public void onCarModeChanged(boolean isCarMode);
-    }
-
-    private final Context mContext;
-    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            Log.startSession("SSP.oR");
-            try {
-                String action = intent.getAction();
-                if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(action)) {
-                    onEnterCarMode();
-                } else if (UiModeManager.ACTION_EXIT_CAR_MODE.equals(action)) {
-                    onExitCarMode();
-                } else {
-                    Log.w(this, "Unexpected intent received: %s", intent.getAction());
-                }
-            } finally {
-                Log.endSession();
-            }
-        }
-    };
-
-    private Set<SystemStateListener> mListeners = new CopyOnWriteArraySet<>();
-    private boolean mIsCarMode;
-
-    public SystemStateProvider(Context context) {
-        mContext = context;
-
-        IntentFilter intentFilter = new IntentFilter(UiModeManager.ACTION_ENTER_CAR_MODE);
-        intentFilter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE);
-        mContext.registerReceiver(mBroadcastReceiver, intentFilter);
-        Log.i(this, "Registering car mode receiver: %s", intentFilter);
-
-        mIsCarMode = getSystemCarMode();
-    }
-
-    public void addListener(SystemStateListener listener) {
-        if (listener != null) {
-            mListeners.add(listener);
-        }
-    }
-
-    public boolean removeListener(SystemStateListener listener) {
-        return mListeners.remove(listener);
-    }
-
-    public boolean isCarMode() {
-        return mIsCarMode;
-    }
-
-    private void onEnterCarMode() {
-        if (!mIsCarMode) {
-            Log.i(this, "Entering carmode");
-            mIsCarMode = true;
-            notifyCarMode();
-        }
-    }
-
-    private void onExitCarMode() {
-        if (mIsCarMode) {
-            Log.i(this, "Exiting carmode");
-            mIsCarMode = false;
-            notifyCarMode();
-        }
-    }
-
-    private void notifyCarMode() {
-        for (SystemStateListener listener : mListeners) {
-            listener.onCarModeChanged(mIsCarMode);
-        }
-    }
-
-    /**
-     * Checks the system for the current car mode.
-     *
-     * @return True if in car mode, false otherwise.
-     */
-    private boolean getSystemCarMode() {
-        UiModeManager uiModeManager =
-                (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
-
-        if (uiModeManager != null) {
-            return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
-        }
-
-        return false;
-    }
-}
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 5a4b328..81d1700 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -47,7 +47,6 @@
 import android.telecom.VideoProfile;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
-import android.text.TextUtils;
 import android.util.EventLog;
 
 import com.android.internal.telecom.ITelecomService;
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index bb7f3fb..6fa7167 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -238,7 +238,7 @@
         mContext.registerReceiver(bluetoothStateReceiver, BluetoothStateReceiver.INTENT_FILTER);
 
         WiredHeadsetManager wiredHeadsetManager = new WiredHeadsetManager(mContext);
-        SystemStateProvider systemStateProvider = new SystemStateProvider(mContext);
+        SystemStateHelper systemStateHelper = new SystemStateHelper(mContext);
 
         mMissedCallNotifier = missedCallNotifierImplFactory
                 .makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar, defaultDialerCache);
@@ -249,7 +249,7 @@
         InCallControllerFactory inCallControllerFactory = new InCallControllerFactory() {
             @Override
             public InCallController create(Context context, SyncRoot lock,
-                    CallsManager callsManager, SystemStateProvider systemStateProvider,
+                    CallsManager callsManager, SystemStateHelper systemStateProvider,
                     DefaultDialerCache defaultDialerCache, Timeouts.Adapter timeoutsAdapter,
                     EmergencyCallHelper emergencyCallHelper) {
                 return new InCallController(context, lock, callsManager, systemStateProvider,
@@ -271,7 +271,7 @@
                 audioServiceFactory,
                 bluetoothRouteManager,
                 wiredHeadsetManager,
-                systemStateProvider,
+                systemStateHelper,
                 defaultDialerCache,
                 timeoutsAdapter,
                 asyncRingtonePlayer,
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index d98de45..7ab6ab2 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -123,8 +123,9 @@
             BluetoothDevice erroneouslyConnectedDevice = getBluetoothAudioConnectedDevice();
             if (erroneouslyConnectedDevice != null) {
                 Log.w(LOG_TAG, "Entering AudioOff state but device %s appears to be connected. " +
-                        "Disconnecting.", erroneouslyConnectedDevice);
-                disconnectAudio();
+                        "Switching to audio-on state for %s", erroneouslyConnectedDevice);
+                // change this to just transition to the new audio on state
+                transitionToActualState();
             }
             cleanupStatesForDisconnectedDevices();
             if (mListener != null) {
diff --git a/tests/Android.mk b/tests/Android.mk
index 395d547..c92b125 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -20,7 +20,7 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-ex-camera2 \
     guava \
-    mockito-target \
+    mockito-target-inline \
     android-support-test \
     platform-test-annotations
 
@@ -51,6 +51,9 @@
 
 LOCAL_USE_AAPT2 := true
 
+LOCAL_JNI_SHARED_LIBRARIES := \
+    libdexmakerjvmtiagent \
+
 LOCAL_AAPT_FLAGS := \
     --auto-add-overlay \
     --extra-packages com.android.server.telecom
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
index 68c5014..56f585f 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -22,6 +22,7 @@
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
 import com.android.server.telecom.CallAudioRouteStateMachine;
+import com.android.server.telecom.SystemStateHelper;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -30,9 +31,10 @@
 import org.mockito.Mock;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -41,6 +43,7 @@
 public class CallAudioModeStateMachineTest extends TelecomTestCase {
     private static final int TEST_TIMEOUT = 1000;
 
+    @Mock private SystemStateHelper mSystemStateHelper;
     @Mock private AudioManager mAudioManager;
     @Mock private CallAudioManager mCallAudioManager;
 
@@ -53,7 +56,8 @@
     @SmallTest
     @Test
     public void testNoFocusWhenRingerSilenced() throws Throwable {
-        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mAudioManager);
+        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
+                mAudioManager);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -84,8 +88,47 @@
 
     @SmallTest
     @Test
+    public void testNoRingWhenDeviceIsAtEar() {
+        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
+                mAudioManager);
+        sm.setCallAudioManager(mCallAudioManager);
+        sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
+        sm.sendMessage(CallAudioModeStateMachine.NEW_HOLDING_CALL,
+                new CallAudioModeStateMachine.MessageArgs(
+                        false, // hasActiveOrDialingCalls
+                        false, // hasRingingCalls
+                        true, // hasHoldingCalls
+                        false, // isTonePlaying
+                        false, // foregroundCallIsVoip
+                        null // session
+                ));
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+        assertEquals(CallAudioModeStateMachine.TONE_HOLD_STATE_NAME, sm.getCurrentStateName());
+        when(mSystemStateHelper.isDeviceAtEar()).thenReturn(true);
+
+        resetMocks();
+        sm.sendMessage(CallAudioModeStateMachine.NEW_RINGING_CALL,
+                new CallAudioModeStateMachine.MessageArgs(
+                        false, // hasActiveOrDialingCalls
+                        true, // hasRingingCalls
+                        true, // hasHoldingCalls
+                        false, // isTonePlaying
+                        false, // foregroundCallIsVoip
+                        null // session
+                ));
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+
+        verify(mAudioManager, never()).requestAudioFocusForCall(anyInt(), anyInt());
+        verify(mAudioManager, never()).setMode(anyInt());
+        verify(mCallAudioManager, never()).startRinging();
+        verify(mCallAudioManager).startCallWaiting(nullable(String.class));
+    }
+
+    @SmallTest
+    @Test
     public void testRegainFocusWhenHfpIsConnectedSilenced() throws Throwable {
-        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mAudioManager);
+        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
+                mAudioManager);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -128,6 +171,6 @@
 
 
     private void resetMocks() {
-        reset(mCallAudioManager, mAudioManager);
+        clearInvocations(mCallAudioManager, mAudioManager);
     }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
index b8b4859..81339ed 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeTransitionTests.java
@@ -16,11 +16,13 @@
 
 package com.android.server.telecom.tests;
 
+import android.content.Context;
 import android.media.AudioManager;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioModeStateMachine;
+import com.android.server.telecom.SystemStateHelper;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -35,6 +37,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
@@ -95,6 +98,7 @@
 
     private static final int TEST_TIMEOUT = 1000;
 
+    @Mock private SystemStateHelper mSystemStateHelper;
     @Mock private AudioManager mAudioManager;
     @Mock private CallAudioManager mCallAudioManager;
     private final ModeTestParameters mParams;
@@ -112,7 +116,8 @@
     @Test
     @SmallTest
     public void modeTransitionTest() {
-        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mAudioManager);
+        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
+                mAudioManager);
         sm.setCallAudioManager(mCallAudioManager);
         sm.sendMessage(mParams.initialAudioState);
         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
@@ -163,11 +168,11 @@
 
         switch (mParams.expectedCallWaitingInteraction) {
             case NO_CHANGE:
-                verify(mCallAudioManager, never()).startCallWaiting();
+                verify(mCallAudioManager, never()).startCallWaiting(nullable(String.class));
                 verify(mCallAudioManager, never()).stopCallWaiting();
                 break;
             case ON:
-                verify(mCallAudioManager).startCallWaiting();
+                verify(mCallAudioManager).startCallWaiting(nullable(String.class));
                 break;
             case OFF:
                 verify(mCallAudioManager).stopCallWaiting();
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index b51794a..bc9cfc3 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -68,7 +68,7 @@
 import com.android.server.telecom.PhoneNumberUtilsAdapter;
 import com.android.server.telecom.ProximitySensorManager;
 import com.android.server.telecom.ProximitySensorManagerFactory;
-import com.android.server.telecom.SystemStateProvider;
+import com.android.server.telecom.SystemStateHelper;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
 import com.android.server.telecom.WiredHeadsetManager;
@@ -131,7 +131,7 @@
     @Mock private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
     @Mock private BluetoothRouteManager mBluetoothRouteManager;
     @Mock private WiredHeadsetManager mWiredHeadsetManager;
-    @Mock private SystemStateProvider mSystemStateProvider;
+    @Mock private SystemStateHelper mSystemStateHelper;
     @Mock private DefaultDialerCache mDefaultDialerCache;
     @Mock private Timeouts.Adapter mTimeoutsAdapter;
     @Mock private AsyncRingtonePlayer mAsyncRingtonePlayer;
@@ -165,7 +165,7 @@
                 any())).thenReturn(mInCallController);
         when(mCallAudioRouteStateMachineFactory.create(any(), any(), any(), any(), any(), any(),
                 anyInt())).thenReturn(mCallAudioRouteStateMachine);
-        when(mCallAudioModeStateMachineFactory.create(any()))
+        when(mCallAudioModeStateMachineFactory.create(any(), any()))
                 .thenReturn(mCallAudioModeStateMachine);
         when(mClockProxy.currentTimeMillis()).thenReturn(System.currentTimeMillis());
         when(mClockProxy.elapsedRealtime()).thenReturn(SystemClock.elapsedRealtime());
@@ -184,7 +184,7 @@
                 mAudioServiceFactory,
                 mBluetoothRouteManager,
                 mWiredHeadsetManager,
-                mSystemStateProvider,
+                mSystemStateHelper,
                 mDefaultDialerCache,
                 mTimeoutsAdapter,
                 mAsyncRingtonePlayer,
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index af08a6f..3be9594 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -525,6 +525,10 @@
         });
     }
 
+    public void putFloatResource(int id, final float value) {
+        when(mResources.getFloat(eq(id))).thenReturn(value);
+    }
+
     public void putBooleanResource(int id, boolean value) {
         when(mResources.getBoolean(eq(id))).thenReturn(value);
     }
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 0671a4e..d76bb6c 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -50,7 +50,7 @@
 import com.android.server.telecom.InCallController;
 import com.android.server.telecom.PhoneAccountRegistrar;
 import com.android.server.telecom.R;
-import com.android.server.telecom.SystemStateProvider;
+import com.android.server.telecom.SystemStateHelper;
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.Timeouts;
 
@@ -88,7 +88,7 @@
     @Mock CallsManager mMockCallsManager;
     @Mock PhoneAccountRegistrar mMockPhoneAccountRegistrar;
     @Mock BluetoothHeadsetProxy mMockBluetoothHeadset;
-    @Mock SystemStateProvider mMockSystemStateProvider;
+    @Mock SystemStateHelper mMockSystemStateHelper;
     @Mock PackageManager mMockPackageManager;
     @Mock Call mMockCall;
     @Mock Resources mMockResources;
@@ -122,7 +122,7 @@
         mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, SYS_PKG,
                 mTimeoutsAdapter);
         mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager,
-                mMockSystemStateProvider, mDefaultDialerCache, mTimeoutsAdapter,
+                mMockSystemStateHelper, mDefaultDialerCache, mTimeoutsAdapter,
                 mEmergencyCallHelper);
     }
 
diff --git a/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java b/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
new file mode 100644
index 0000000..efe8796
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.UiModeManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.SystemStateHelper;
+import com.android.server.telecom.SystemStateHelper.SystemStateListener;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.internal.util.reflection.FieldSetter;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+/**
+ * Unit tests for SystemStateHelper
+ */
+@RunWith(JUnit4.class)
+public class SystemStateHelperTest extends TelecomTestCase {
+
+    Context mContext;
+    @Mock SystemStateListener mSystemStateListener;
+    @Mock Sensor mGravitySensor;
+    @Mock Sensor mProxSensor;
+    @Mock UiModeManager mUiModeManager;
+    @Mock SensorManager mSensorManager;
+    @Mock Intent mIntentEnter;
+    @Mock Intent mIntentExit;
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+        doReturn(mSensorManager).when(mContext).getSystemService(SensorManager.class);
+        when(mGravitySensor.getType()).thenReturn(Sensor.TYPE_GRAVITY);
+        when(mProxSensor.getType()).thenReturn(Sensor.TYPE_PROXIMITY);
+        when(mProxSensor.getMaximumRange()).thenReturn(5.0f);
+        when(mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)).thenReturn(mGravitySensor);
+        when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(mProxSensor);
+
+        mComponentContextFixture.putFloatResource(
+                R.dimen.device_on_ear_xy_gravity_threshold, 5.5f);
+        mComponentContextFixture.putFloatResource(
+                R.dimen.device_on_ear_y_gravity_negative_threshold, -1f);
+    }
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @SmallTest
+    @Test
+    public void testListeners() throws Exception {
+        SystemStateHelper systemStateHelper = new SystemStateHelper(mContext);
+
+        assertFalse(systemStateHelper.removeListener(mSystemStateListener));
+        systemStateHelper.addListener(mSystemStateListener);
+        assertTrue(systemStateHelper.removeListener(mSystemStateListener));
+        assertFalse(systemStateHelper.removeListener(mSystemStateListener));
+    }
+
+    @SmallTest
+    @Test
+    public void testQuerySystemForCarMode_True() {
+        when(mContext.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(mUiModeManager);
+        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        assertTrue(new SystemStateHelper(mContext).isCarMode());
+    }
+
+    @SmallTest
+    @Test
+    public void testQuerySystemForCarMode_False() {
+        when(mContext.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(mUiModeManager);
+        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_NORMAL);
+        assertFalse(new SystemStateHelper(mContext).isCarMode());
+    }
+
+    @SmallTest
+    @Test
+    public void testReceiverAndIntentFilter() {
+        ArgumentCaptor<IntentFilter> intentFilter = ArgumentCaptor.forClass(IntentFilter.class);
+        new SystemStateHelper(mContext);
+        verify(mContext).registerReceiver(any(BroadcastReceiver.class), intentFilter.capture());
+
+        assertEquals(2, intentFilter.getValue().countActions());
+        assertEquals(UiModeManager.ACTION_ENTER_CAR_MODE, intentFilter.getValue().getAction(0));
+        assertEquals(UiModeManager.ACTION_EXIT_CAR_MODE, intentFilter.getValue().getAction(1));
+    }
+
+    @SmallTest
+    @Test
+    public void testOnEnterExitCarMode() {
+        ArgumentCaptor<BroadcastReceiver> receiver =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        new SystemStateHelper(mContext).addListener(mSystemStateListener);
+
+        verify(mContext).registerReceiver(receiver.capture(), any(IntentFilter.class));
+
+        when(mIntentEnter.getAction()).thenReturn(UiModeManager.ACTION_ENTER_CAR_MODE);
+        receiver.getValue().onReceive(mContext, mIntentEnter);
+        verify(mSystemStateListener).onCarModeChanged(true);
+
+        when(mIntentExit.getAction()).thenReturn(UiModeManager.ACTION_EXIT_CAR_MODE);
+        receiver.getValue().onReceive(mContext, mIntentExit);
+        verify(mSystemStateListener).onCarModeChanged(false);
+
+        receiver.getValue().onReceive(mContext, new Intent("invalid action"));
+    }
+
+    @SmallTest
+    @Test
+    public void testDeviceOnEarCorrectlyDetected() {
+        doAnswer(invocation -> {
+            SensorEventListener listener = invocation.getArgument(0);
+            Sensor sensor = invocation.getArgument(1);
+            if (sensor.getType() == Sensor.TYPE_GRAVITY) {
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{1.0f, 9.0f, 1.0f}, Sensor.TYPE_GRAVITY));
+            } else {
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{0.0f}, Sensor.TYPE_PROXIMITY));
+            }
+            return true;
+        }).when(mSensorManager)
+                .registerListener(any(SensorEventListener.class), any(Sensor.class), anyInt());
+
+        assertTrue(SystemStateHelper.isDeviceAtEar(mContext));
+        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+    }
+
+    @SmallTest
+    @Test
+    public void testDeviceIsNotOnEarWithProxNotSensed() {
+        doAnswer(invocation -> {
+            SensorEventListener listener = invocation.getArgument(0);
+            Sensor sensor = invocation.getArgument(1);
+            if (sensor.getType() == Sensor.TYPE_GRAVITY) {
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{1.0f, 9.0f, 1.0f}, Sensor.TYPE_GRAVITY));
+            } else {
+                // do nothing to simulate proximity sensor not reporting
+            }
+            return true;
+        }).when(mSensorManager)
+                .registerListener(any(SensorEventListener.class), any(Sensor.class), anyInt());
+
+        assertFalse(SystemStateHelper.isDeviceAtEar(mContext));
+        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+    }
+
+    @SmallTest
+    @Test
+    public void testDeviceIsNotOnEarWithWrongOrientation() {
+        doAnswer(invocation -> {
+            SensorEventListener listener = invocation.getArgument(0);
+            Sensor sensor = invocation.getArgument(1);
+            if (sensor.getType() == Sensor.TYPE_GRAVITY) {
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{1.0f, 1.0f, 9.0f}, Sensor.TYPE_GRAVITY));
+            } else {
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{0.0f}, Sensor.TYPE_PROXIMITY));
+            }
+            return true;
+        }).when(mSensorManager)
+                .registerListener(any(SensorEventListener.class), any(Sensor.class), anyInt());
+
+        assertFalse(SystemStateHelper.isDeviceAtEar(mContext));
+        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+    }
+
+    @SmallTest
+    @Test
+    public void testDeviceIsNotOnEarWithMissingSensor() {
+        when(mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)).thenReturn(null);
+        doAnswer(invocation -> {
+            SensorEventListener listener = invocation.getArgument(0);
+            Sensor sensor = invocation.getArgument(1);
+            if (sensor.getType() == Sensor.TYPE_GRAVITY) {
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{1.0f, 9.0f, 1.0f}, Sensor.TYPE_GRAVITY));
+            } else {
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{0.0f}, Sensor.TYPE_PROXIMITY));
+            }
+            return true;
+        }).when(mSensorManager)
+                .registerListener(any(SensorEventListener.class), any(Sensor.class), anyInt());
+
+        assertFalse(SystemStateHelper.isDeviceAtEar(mContext));
+    }
+
+    @SmallTest
+    @Test
+    public void testDeviceIsNotOnEarWithTimeout() {
+        doAnswer(invocation -> {
+            SensorEventListener listener = invocation.getArgument(0);
+            Sensor sensor = invocation.getArgument(1);
+            if (sensor.getType() == Sensor.TYPE_GRAVITY) {
+                // do nothing
+            } else {
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{0.0f}, Sensor.TYPE_PROXIMITY));
+            }
+            return true;
+        }).when(mSensorManager)
+                .registerListener(any(SensorEventListener.class), any(Sensor.class), anyInt());
+
+        assertFalse(SystemStateHelper.isDeviceAtEar(mContext));
+    }
+
+    @SmallTest
+    @Test
+    public void testDeviceIsOnEarWithMultiSensorInputs() {
+        doAnswer(invocation -> {
+            SensorEventListener listener = invocation.getArgument(0);
+            Sensor sensor = invocation.getArgument(1);
+            if (sensor.getType() == Sensor.TYPE_GRAVITY) {
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{1.0f, 9.0f, 1.0f}, Sensor.TYPE_GRAVITY));
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{1.0f, -9.0f, 1.0f}, Sensor.TYPE_GRAVITY));
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{1.0f, 0.0f, 8.0f}, Sensor.TYPE_GRAVITY));
+            } else {
+                listener.onSensorChanged(makeSensorEvent(
+                        new float[]{0.0f}, Sensor.TYPE_PROXIMITY));
+            }
+            return true;
+        }).when(mSensorManager)
+                .registerListener(any(SensorEventListener.class), any(Sensor.class), anyInt());
+
+        assertTrue(SystemStateHelper.isDeviceAtEar(mContext));
+        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+    }
+
+    private SensorEvent makeSensorEvent(float[] values, int sensorType) throws Exception {
+        SensorEvent event = mock(SensorEvent.class);
+        Sensor mockSensor = mock(Sensor.class);
+        when(mockSensor.getType()).thenReturn(sensorType);
+        FieldSetter.setField(event, SensorEvent.class.getDeclaredField("sensor"), mockSensor);
+        FieldSetter.setField(event, SensorEvent.class.getDeclaredField("values"), values);
+        return event;
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/SystemStateProviderTest.java b/tests/src/com/android/server/telecom/tests/SystemStateProviderTest.java
deleted file mode 100644
index 033f929..0000000
--- a/tests/src/com/android/server/telecom/tests/SystemStateProviderTest.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.server.telecom.tests;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import com.android.server.telecom.SystemStateProvider;
-import com.android.server.telecom.SystemStateProvider.SystemStateListener;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * Unit tests for SystemStateProvider
- */
-@RunWith(JUnit4.class)
-public class SystemStateProviderTest extends TelecomTestCase {
-
-    @Mock Context mContext;
-    @Mock SystemStateListener mSystemStateListener;
-    @Mock UiModeManager mUiModeManager;
-    @Mock Intent mIntentEnter;
-    @Mock Intent mIntentExit;
-
-    @Override
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Override
-    @After
-    public void tearDown() throws Exception {
-        super.tearDown();
-    }
-
-    @SmallTest
-    @Test
-    public void testListeners() throws Exception {
-        SystemStateProvider systemStateProvider = new SystemStateProvider(mContext);
-
-        assertFalse(systemStateProvider.removeListener(mSystemStateListener));
-        systemStateProvider.addListener(mSystemStateListener);
-        assertTrue(systemStateProvider.removeListener(mSystemStateListener));
-        assertFalse(systemStateProvider.removeListener(mSystemStateListener));
-    }
-
-    @SmallTest
-    @Test
-    public void testQuerySystemForCarMode_True() {
-        when(mContext.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(mUiModeManager);
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
-        assertTrue(new SystemStateProvider(mContext).isCarMode());
-    }
-
-    @SmallTest
-    @Test
-    public void testQuerySystemForCarMode_False() {
-        when(mContext.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(mUiModeManager);
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_NORMAL);
-        assertFalse(new SystemStateProvider(mContext).isCarMode());
-    }
-
-    @SmallTest
-    @Test
-    public void testReceiverAndIntentFilter() {
-        ArgumentCaptor<IntentFilter> intentFilter = ArgumentCaptor.forClass(IntentFilter.class);
-        new SystemStateProvider(mContext);
-        verify(mContext).registerReceiver(any(BroadcastReceiver.class), intentFilter.capture());
-
-        assertEquals(2, intentFilter.getValue().countActions());
-        assertEquals(UiModeManager.ACTION_ENTER_CAR_MODE, intentFilter.getValue().getAction(0));
-        assertEquals(UiModeManager.ACTION_EXIT_CAR_MODE, intentFilter.getValue().getAction(1));
-    }
-
-    @SmallTest
-    @Test
-    public void testOnEnterExitCarMode() {
-        ArgumentCaptor<BroadcastReceiver> receiver =
-                ArgumentCaptor.forClass(BroadcastReceiver.class);
-        new SystemStateProvider(mContext).addListener(mSystemStateListener);
-
-        verify(mContext).registerReceiver(receiver.capture(), any(IntentFilter.class));
-
-        when(mIntentEnter.getAction()).thenReturn(UiModeManager.ACTION_ENTER_CAR_MODE);
-        receiver.getValue().onReceive(mContext, mIntentEnter);
-        verify(mSystemStateListener).onCarModeChanged(true);
-
-        when(mIntentExit.getAction()).thenReturn(UiModeManager.ACTION_EXIT_CAR_MODE);
-        receiver.getValue().onReceive(mContext, mIntentExit);
-        verify(mSystemStateListener).onCarModeChanged(false);
-
-        receiver.getValue().onReceive(mContext, new Intent("invalid action"));
-    }
-}