1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.server.telecom; 18 19 import android.app.UiModeManager; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.content.res.Configuration; 25 import android.hardware.Sensor; 26 import android.hardware.SensorEvent; 27 import android.hardware.SensorEventListener; 28 import android.hardware.SensorManager; 29 import android.telecom.Log; 30 31 import java.util.Set; 32 import java.util.concurrent.CopyOnWriteArraySet; 33 import java.util.concurrent.CountDownLatch; 34 import java.util.concurrent.TimeUnit; 35 import java.util.concurrent.atomic.AtomicBoolean; 36 37 /** 38 * Provides various system states to the rest of the telecom codebase. 39 */ 40 public class SystemStateHelper { 41 public static interface SystemStateListener { onCarModeChanged(boolean isCarMode)42 public void onCarModeChanged(boolean isCarMode); 43 } 44 45 private final Context mContext; 46 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 47 @Override 48 public void onReceive(Context context, Intent intent) { 49 Log.startSession("SSP.oR"); 50 try { 51 String action = intent.getAction(); 52 if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(action)) { 53 onEnterCarMode(); 54 } else if (UiModeManager.ACTION_EXIT_CAR_MODE.equals(action)) { 55 onExitCarMode(); 56 } else { 57 Log.w(this, "Unexpected intent received: %s", intent.getAction()); 58 } 59 } finally { 60 Log.endSession(); 61 } 62 } 63 }; 64 65 private Set<SystemStateListener> mListeners = new CopyOnWriteArraySet<>(); 66 private boolean mIsCarMode; 67 SystemStateHelper(Context context)68 public SystemStateHelper(Context context) { 69 mContext = context; 70 71 IntentFilter intentFilter = new IntentFilter(UiModeManager.ACTION_ENTER_CAR_MODE); 72 intentFilter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE); 73 mContext.registerReceiver(mBroadcastReceiver, intentFilter); 74 Log.i(this, "Registering car mode receiver: %s", intentFilter); 75 76 mIsCarMode = getSystemCarMode(); 77 } 78 addListener(SystemStateListener listener)79 public void addListener(SystemStateListener listener) { 80 if (listener != null) { 81 mListeners.add(listener); 82 } 83 } 84 removeListener(SystemStateListener listener)85 public boolean removeListener(SystemStateListener listener) { 86 return mListeners.remove(listener); 87 } 88 isCarMode()89 public boolean isCarMode() { 90 return mIsCarMode; 91 } 92 isDeviceAtEar()93 public boolean isDeviceAtEar() { 94 return isDeviceAtEar(mContext); 95 } 96 97 /** 98 * Returns a guess whether the phone is up to the user's ear. Use the proximity sensor and 99 * the gravity sensor to make a guess 100 * @return true if the proximity sensor is activated, the magnitude of gravity in directions 101 * parallel to the screen is greater than some configurable threshold, and the 102 * y-component of gravity isn't less than some other configurable threshold. 103 */ isDeviceAtEar(Context context)104 public static boolean isDeviceAtEar(Context context) { 105 SensorManager sm = context.getSystemService(SensorManager.class); 106 if (sm == null) { 107 return false; 108 } 109 Sensor grav = sm.getDefaultSensor(Sensor.TYPE_GRAVITY); 110 Sensor proximity = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY); 111 if (grav == null || proximity == null) { 112 return false; 113 } 114 115 AtomicBoolean result = new AtomicBoolean(true); 116 CountDownLatch gravLatch = new CountDownLatch(1); 117 CountDownLatch proxLatch = new CountDownLatch(1); 118 119 final double xyGravityThreshold = context.getResources().getFloat( 120 R.dimen.device_on_ear_xy_gravity_threshold); 121 final double yGravityNegativeThreshold = context.getResources().getFloat( 122 R.dimen.device_on_ear_y_gravity_negative_threshold); 123 124 SensorEventListener listener = new SensorEventListener() { 125 @Override 126 public void onSensorChanged(SensorEvent event) { 127 if (event.sensor.getType() == Sensor.TYPE_GRAVITY) { 128 if (gravLatch.getCount() == 0) { 129 return; 130 } 131 double xyMag = Math.sqrt(event.values[0] * event.values[0] 132 + event.values[1] * event.values[1]); 133 if (xyMag < xyGravityThreshold 134 || event.values[1] < yGravityNegativeThreshold) { 135 result.set(false); 136 } 137 gravLatch.countDown(); 138 } else if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) { 139 if (proxLatch.getCount() == 0) { 140 return; 141 } 142 if (event.values[0] >= proximity.getMaximumRange()) { 143 result.set(false); 144 } 145 proxLatch.countDown(); 146 } 147 } 148 149 @Override 150 public void onAccuracyChanged(Sensor sensor, int accuracy) { 151 } 152 }; 153 154 try { 155 sm.registerListener(listener, grav, SensorManager.SENSOR_DELAY_FASTEST); 156 sm.registerListener(listener, proximity, SensorManager.SENSOR_DELAY_FASTEST); 157 boolean accelValid = gravLatch.await(100, TimeUnit.MILLISECONDS); 158 boolean proxValid = proxLatch.await(100, TimeUnit.MILLISECONDS); 159 if (accelValid && proxValid) { 160 return result.get(); 161 } else { 162 Log.w(SystemStateHelper.class.getSimpleName(), 163 "Timed out waiting for sensors: %b %b", accelValid, proxValid); 164 return false; 165 } 166 } catch (InterruptedException e) { 167 return false; 168 } finally { 169 sm.unregisterListener(listener); 170 } 171 } 172 onEnterCarMode()173 private void onEnterCarMode() { 174 if (!mIsCarMode) { 175 Log.i(this, "Entering carmode"); 176 mIsCarMode = true; 177 notifyCarMode(); 178 } 179 } 180 onExitCarMode()181 private void onExitCarMode() { 182 if (mIsCarMode) { 183 Log.i(this, "Exiting carmode"); 184 mIsCarMode = false; 185 notifyCarMode(); 186 } 187 } 188 notifyCarMode()189 private void notifyCarMode() { 190 for (SystemStateListener listener : mListeners) { 191 listener.onCarModeChanged(mIsCarMode); 192 } 193 } 194 195 /** 196 * Checks the system for the current car mode. 197 * 198 * @return True if in car mode, false otherwise. 199 */ getSystemCarMode()200 private boolean getSystemCarMode() { 201 UiModeManager uiModeManager = 202 (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE); 203 204 if (uiModeManager != null) { 205 return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR; 206 } 207 208 return false; 209 } 210 } 211