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 */ onCarModeChanged(int priority, String packageName, boolean isCarMode)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 */ onAutomotiveProjectionStateSet(String automotiveProjectionPackage)59 void onAutomotiveProjectionStateSet(String automotiveProjectionPackage); 60 61 /** 62 * Listener method to inform interested parties when automotive projection state has been 63 * cleared. 64 */ onAutomotiveProjectionStateReleased()65 void onAutomotiveProjectionStateReleased(); 66 67 /** 68 * Notifies when a package has been uninstalled. 69 * @param packageName the package name of the uninstalled package 70 */ onPackageUninstalled(String packageName)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 onProjectionStateChanged(int activeProjectionTypes, @NonNull Set<String> projectingPackages)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 SystemStateHelper(Context context, TelecomSystem.SyncRoot lock)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 intentFilter1.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); 150 151 IntentFilter intentFilter2 = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED); 152 intentFilter2.addDataScheme("package"); 153 intentFilter2.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); 154 mContext.registerReceiver(mBroadcastReceiver, intentFilter1); 155 mContext.registerReceiver(mBroadcastReceiver, intentFilter2); 156 Log.i(this, "Registering broadcast receiver: %s", intentFilter1); 157 Log.i(this, "Registering broadcast receiver: %s", intentFilter2); 158 159 mContext.getSystemService(UiModeManager.class).addOnProjectionStateChangedListener( 160 UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, mContext.getMainExecutor(), this); 161 mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); 162 } 163 addListener(SystemStateListener listener)164 public void addListener(SystemStateListener listener) { 165 if (listener != null) { 166 mListeners.add(listener); 167 } 168 } 169 removeListener(SystemStateListener listener)170 public boolean removeListener(SystemStateListener listener) { 171 return mListeners.remove(listener); 172 } 173 isCarModeOrProjectionActive()174 public boolean isCarModeOrProjectionActive() { 175 return mIsCarModeOrProjectionActive; 176 } 177 isDeviceAtEar()178 public boolean isDeviceAtEar() { 179 return isDeviceAtEar(mContext); 180 } 181 182 /** 183 * Returns a guess whether the phone is up to the user's ear. Use the proximity sensor and 184 * the gravity sensor to make a guess 185 * @return true if the proximity sensor is activated, the magnitude of gravity in directions 186 * parallel to the screen is greater than some configurable threshold, and the 187 * y-component of gravity isn't less than some other configurable threshold. 188 */ isDeviceAtEar(Context context)189 public static boolean isDeviceAtEar(Context context) { 190 SensorManager sm = context.getSystemService(SensorManager.class); 191 if (sm == null) { 192 return false; 193 } 194 Sensor grav = sm.getDefaultSensor(Sensor.TYPE_GRAVITY); 195 Sensor proximity = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY); 196 if (grav == null || proximity == null) { 197 return false; 198 } 199 200 AtomicBoolean result = new AtomicBoolean(true); 201 CountDownLatch gravLatch = new CountDownLatch(1); 202 CountDownLatch proxLatch = new CountDownLatch(1); 203 204 final double xyGravityThreshold = context.getResources().getFloat( 205 R.dimen.device_on_ear_xy_gravity_threshold); 206 final double yGravityNegativeThreshold = context.getResources().getFloat( 207 R.dimen.device_on_ear_y_gravity_negative_threshold); 208 209 SensorEventListener listener = new SensorEventListener() { 210 @Override 211 public void onSensorChanged(SensorEvent event) { 212 if (event.sensor.getType() == Sensor.TYPE_GRAVITY) { 213 if (gravLatch.getCount() == 0) { 214 return; 215 } 216 double xyMag = Math.sqrt(event.values[0] * event.values[0] 217 + event.values[1] * event.values[1]); 218 if (xyMag < xyGravityThreshold 219 || event.values[1] < yGravityNegativeThreshold) { 220 result.set(false); 221 } 222 gravLatch.countDown(); 223 } else if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) { 224 if (proxLatch.getCount() == 0) { 225 return; 226 } 227 if (event.values[0] >= proximity.getMaximumRange()) { 228 result.set(false); 229 } 230 proxLatch.countDown(); 231 } 232 } 233 234 @Override 235 public void onAccuracyChanged(Sensor sensor, int accuracy) { 236 } 237 }; 238 239 try { 240 sm.registerListener(listener, grav, SensorManager.SENSOR_DELAY_FASTEST); 241 sm.registerListener(listener, proximity, SensorManager.SENSOR_DELAY_FASTEST); 242 boolean accelValid = gravLatch.await(100, TimeUnit.MILLISECONDS); 243 boolean proxValid = proxLatch.await(100, TimeUnit.MILLISECONDS); 244 if (accelValid && proxValid) { 245 return result.get(); 246 } else { 247 Log.w(SystemStateHelper.class.getSimpleName(), 248 "Timed out waiting for sensors: %b %b", accelValid, proxValid); 249 return false; 250 } 251 } catch (InterruptedException e) { 252 return false; 253 } finally { 254 sm.unregisterListener(listener); 255 } 256 } 257 onEnterCarMode(int priority, String packageName)258 private void onEnterCarMode(int priority, String packageName) { 259 Log.i(this, "Entering carmode"); 260 mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); 261 for (SystemStateListener listener : mListeners) { 262 listener.onCarModeChanged(priority, packageName, true /* isCarMode */); 263 } 264 } 265 onExitCarMode(int priority, String packageName)266 private void onExitCarMode(int priority, String packageName) { 267 Log.i(this, "Exiting carmode"); 268 mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); 269 for (SystemStateListener listener : mListeners) { 270 listener.onCarModeChanged(priority, packageName, false /* isCarMode */); 271 } 272 } 273 onSetAutomotiveProjection(String packageName)274 private void onSetAutomotiveProjection(String packageName) { 275 Log.i(this, "Automotive projection set."); 276 mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); 277 for (SystemStateListener listener : mListeners) { 278 listener.onAutomotiveProjectionStateSet(packageName); 279 } 280 281 } 282 onReleaseAutomotiveProjection()283 private void onReleaseAutomotiveProjection() { 284 Log.i(this, "Automotive projection released."); 285 mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState(); 286 for (SystemStateListener listener : mListeners) { 287 listener.onAutomotiveProjectionStateReleased(); 288 } 289 } 290 291 /** 292 * Checks the system for the current car projection state. 293 * 294 * @return True if projection is active, false otherwise. 295 */ getSystemCarModeOrProjectionState()296 private boolean getSystemCarModeOrProjectionState() { 297 UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class); 298 299 if (uiModeManager != null) { 300 return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR 301 || (uiModeManager.getActiveProjectionTypes() 302 & UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) != 0; 303 } 304 305 Log.w(this, "Got null UiModeManager, returning false."); 306 return false; 307 } 308 } 309