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