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