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.annotation.NonNull; 20 import android.app.UiModeManager; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.res.Configuration; 26 import android.hardware.Sensor; 27 import android.hardware.SensorEvent; 28 import android.hardware.SensorEventListener; 29 import android.hardware.SensorManager; 30 import android.net.Uri; 31 import android.telecom.Log; 32 33 import java.util.Set; 34 import java.util.concurrent.CopyOnWriteArraySet; 35 import java.util.concurrent.CountDownLatch; 36 import java.util.concurrent.TimeUnit; 37 import java.util.concurrent.atomic.AtomicBoolean; 38 39 /** 40 * Provides various system states to the rest of the telecom codebase. 41 */ 42 public class SystemStateHelper implements UiModeManager.OnProjectionStateChangedListener { 43 public interface SystemStateListener { 44 /** 45 * Listener method to inform interested parties when a package name requests to enter or 46 * exit car mode. 47 * @param priority the priority of the enter/exit request. 48 * @param packageName the package name of the requester. 49 * @param isCarMode {@code true} if the package is entering car mode, {@code false} 50 * otherwise. 51 */ 52 void onCarModeChanged(int priority, String packageName, boolean isCarMode); 53 54 /** 55 * Listener method to inform interested parties when a package has set automotive projection 56 * state. 57 * @param automotiveProjectionPackage the package that set automotive projection. 58 */ 59 void onAutomotiveProjectionStateSet(String automotiveProjectionPackage); 60 61 /** 62 * Listener method to inform interested parties when automotive projection state has been 63 * cleared. 64 */ 65 void onAutomotiveProjectionStateReleased(); 66 67 /** 68 * Notifies when a package has been uninstalled. 69 * @param packageName the package name of the uninstalled package 70 */ 71 void onPackageUninstalled(String packageName); 72 } 73 74 private final Context mContext; 75 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 76 @Override 77 public void onReceive(Context context, Intent intent) { 78 Log.startSession("SSH.oR"); 79 try { 80 synchronized (mLock) { 81 String action = intent.getAction(); 82 if (UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED.equals(action)) { 83 int priority = intent.getIntExtra(UiModeManager.EXTRA_PRIORITY, 84 UiModeManager.DEFAULT_PRIORITY); 85 String callingPackage = intent.getStringExtra( 86 UiModeManager.EXTRA_CALLING_PACKAGE); 87 Log.i(SystemStateHelper.this, 88 "ENTER_CAR_MODE_PRIORITIZED; priority=%d, pkg=%s", 89 priority, callingPackage); 90 onEnterCarMode(priority, callingPackage); 91 } else if (UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED.equals(action)) { 92 int priority = intent.getIntExtra(UiModeManager.EXTRA_PRIORITY, 93 UiModeManager.DEFAULT_PRIORITY); 94 String callingPackage = intent.getStringExtra( 95 UiModeManager.EXTRA_CALLING_PACKAGE); 96 Log.i(SystemStateHelper.this, 97 "EXIT_CAR_MODE_PRIORITIZED; priority=%d, pkg=%s", 98 priority, callingPackage); 99 onExitCarMode(priority, callingPackage); 100 } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { 101 Uri data = intent.getData(); 102 if (data == null) { 103 Log.w(SystemStateHelper.this, 104 "Got null data for package removed, ignoring"); 105 return; 106 } 107 mListeners.forEach( 108 l -> l.onPackageUninstalled(data.getEncodedSchemeSpecificPart())); 109 } else { 110 Log.w(SystemStateHelper.this, 111 "Unexpected intent received: %s", intent.getAction()); 112 } 113 } 114 } finally { 115 Log.endSession(); 116 } 117 } 118 }; 119 120 @Override 121 public void onProjectionStateChanged(int activeProjectionTypes, 122 @NonNull Set<String> projectingPackages) { 123 Log.startSession("SSH.oPSC"); 124 try { 125 synchronized (mLock) { 126 if (projectingPackages.isEmpty()) { 127 onReleaseAutomotiveProjection(); 128 } else { 129 onSetAutomotiveProjection(projectingPackages.iterator().next()); 130 } 131 } 132 } finally { 133 Log.endSession(); 134 } 135 136 } 137 138 private Set<SystemStateListener> mListeners = new CopyOnWriteArraySet<>(); 139 private boolean mIsCarModeOrProjectionActive; 140 private final TelecomSystem.SyncRoot mLock; 141 142 public SystemStateHelper(Context context, TelecomSystem.SyncRoot lock) { 143 mContext = context; 144 mLock = lock; 145 146 IntentFilter intentFilter1 = new IntentFilter( 147 UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED); 148 intentFilter1.addAction(UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED); 149 150 IntentFilter intentFilter2 = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED); 151 intentFilter2.addDataScheme("package"); 152 mContext.registerReceiver(mBroadcastReceiver, intentFilter1); 153 mContext.registerReceiver(mBroadcastReceiver, intentFilter2); 154 Log.i(this, "Registering broadcast receiver: %s", intentFilter1); 155 Log.i(this, "Registering broadcast receiver: %s", intentFilter2); 156 157 mContext.getSystemService(UiModeManager.class).addOnProjectionStateChangedListener( 158 UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, mContext.getMainExecutor(), this); 159 mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); 160 } 161 162 public void addListener(SystemStateListener listener) { 163 if (listener != null) { 164 mListeners.add(listener); 165 } 166 } 167 168 public boolean removeListener(SystemStateListener listener) { 169 return mListeners.remove(listener); 170 } 171 172 public boolean isCarModeOrProjectionActive() { 173 return mIsCarModeOrProjectionActive; 174 } 175 176 public boolean isDeviceAtEar() { 177 return isDeviceAtEar(mContext); 178 } 179 180 /** 181 * Returns a guess whether the phone is up to the user's ear. Use the proximity sensor and 182 * the gravity sensor to make a guess 183 * @return true if the proximity sensor is activated, the magnitude of gravity in directions 184 * parallel to the screen is greater than some configurable threshold, and the 185 * y-component of gravity isn't less than some other configurable threshold. 186 */ 187 public static boolean isDeviceAtEar(Context context) { 188 SensorManager sm = context.getSystemService(SensorManager.class); 189 if (sm == null) { 190 return false; 191 } 192 Sensor grav = sm.getDefaultSensor(Sensor.TYPE_GRAVITY); 193 Sensor proximity = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY); 194 if (grav == null || proximity == null) { 195 return false; 196 } 197 198 AtomicBoolean result = new AtomicBoolean(true); 199 CountDownLatch gravLatch = new CountDownLatch(1); 200 CountDownLatch proxLatch = new CountDownLatch(1); 201 202 final double xyGravityThreshold = context.getResources().getFloat( 203 R.dimen.device_on_ear_xy_gravity_threshold); 204 final double yGravityNegativeThreshold = context.getResources().getFloat( 205 R.dimen.device_on_ear_y_gravity_negative_threshold); 206 207 SensorEventListener listener = new SensorEventListener() { 208 @Override 209 public void onSensorChanged(SensorEvent event) { 210 if (event.sensor.getType() == Sensor.TYPE_GRAVITY) { 211 if (gravLatch.getCount() == 0) { 212 return; 213 } 214 double xyMag = Math.sqrt(event.values[0] * event.values[0] 215 + event.values[1] * event.values[1]); 216 if (xyMag < xyGravityThreshold 217 || event.values[1] < yGravityNegativeThreshold) { 218 result.set(false); 219 } 220 gravLatch.countDown(); 221 } else if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) { 222 if (proxLatch.getCount() == 0) { 223 return; 224 } 225 if (event.values[0] >= proximity.getMaximumRange()) { 226 result.set(false); 227 } 228 proxLatch.countDown(); 229 } 230 } 231 232 @Override 233 public void onAccuracyChanged(Sensor sensor, int accuracy) { 234 } 235 }; 236 237 try { 238 sm.registerListener(listener, grav, SensorManager.SENSOR_DELAY_FASTEST); 239 sm.registerListener(listener, proximity, SensorManager.SENSOR_DELAY_FASTEST); 240 boolean accelValid = gravLatch.await(100, TimeUnit.MILLISECONDS); 241 boolean proxValid = proxLatch.await(100, TimeUnit.MILLISECONDS); 242 if (accelValid && proxValid) { 243 return result.get(); 244 } else { 245 Log.w(SystemStateHelper.class.getSimpleName(), 246 "Timed out waiting for sensors: %b %b", accelValid, proxValid); 247 return false; 248 } 249 } catch (InterruptedException e) { 250 return false; 251 } finally { 252 sm.unregisterListener(listener); 253 } 254 } 255 256 private void onEnterCarMode(int priority, String packageName) { 257 Log.i(this, "Entering carmode"); 258 mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); 259 for (SystemStateListener listener : mListeners) { 260 listener.onCarModeChanged(priority, packageName, true /* isCarMode */); 261 } 262 } 263 264 private void onExitCarMode(int priority, String packageName) { 265 Log.i(this, "Exiting carmode"); 266 mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); 267 for (SystemStateListener listener : mListeners) { 268 listener.onCarModeChanged(priority, packageName, false /* isCarMode */); 269 } 270 } 271 272 private void onSetAutomotiveProjection(String packageName) { 273 Log.i(this, "Automotive projection set."); 274 mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); 275 for (SystemStateListener listener : mListeners) { 276 listener.onAutomotiveProjectionStateSet(packageName); 277 } 278 279 } 280 281 private void onReleaseAutomotiveProjection() { 282 Log.i(this, "Automotive projection released."); 283 mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); 284 for (SystemStateListener listener : mListeners) { 285 listener.onAutomotiveProjectionStateReleased(); 286 } 287 } 288 289 /** 290 * Checks the system for the current car projection state. 291 * 292 * @return True if projection is active, false otherwise. 293 */ 294 private boolean getSystemCarModeOrProjectionState() { 295 UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class); 296 297 if (uiModeManager != null) { 298 return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR 299 || (uiModeManager.getActiveProjectionTypes() 300 & UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) != 0; 301 } 302 303 Log.w(this, "Got null UiModeManager, returning false."); 304 return false; 305 } 306 } 307