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 {
42         /**
43          * Listener method to inform interested parties when a package name requests to enter or
44          * exit car mode.
45          * @param priority the priority of the enter/exit request.
46          * @param packageName the package name of the requester.
47          * @param isCarMode {@code true} if the package is entering car mode, {@code false}
48          *                              otherwise.
49          */
onCarModeChanged(int priority, String packageName, boolean isCarMode)50         void onCarModeChanged(int priority, String packageName, boolean isCarMode);
51     }
52 
53     private final Context mContext;
54     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
55         @Override
56         public void onReceive(Context context, Intent intent) {
57             Log.startSession("SSP.oR");
58             try {
59                 String action = intent.getAction();
60                 if (UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED.equals(action)) {
61                     int priority = intent.getIntExtra(UiModeManager.EXTRA_PRIORITY,
62                             UiModeManager.DEFAULT_PRIORITY);
63                     String callingPackage = intent.getStringExtra(
64                             UiModeManager.EXTRA_CALLING_PACKAGE);
65                     Log.i(SystemStateHelper.this, "ENTER_CAR_MODE_PRIVILEGED; priority=%d, pkg=%s",
66                             priority, callingPackage);
67                     onEnterCarMode(priority, callingPackage);
68                 } else if (UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED.equals(action)) {
69                     int priority = intent.getIntExtra(UiModeManager.EXTRA_PRIORITY,
70                             UiModeManager.DEFAULT_PRIORITY);
71                     String callingPackage = intent.getStringExtra(
72                             UiModeManager.EXTRA_CALLING_PACKAGE);
73                     Log.i(SystemStateHelper.this, "EXIT_CAR_MODE_PRIVILEGED; priority=%d, pkg=%s",
74                             priority, callingPackage);
75                     onExitCarMode(priority, callingPackage);
76                 } else {
77                     Log.w(this, "Unexpected intent received: %s", intent.getAction());
78                 }
79             } finally {
80                 Log.endSession();
81             }
82         }
83     };
84 
85     private Set<SystemStateListener> mListeners = new CopyOnWriteArraySet<>();
86     private boolean mIsCarMode;
87 
SystemStateHelper(Context context)88     public SystemStateHelper(Context context) {
89         mContext = context;
90 
91         IntentFilter intentFilter = new IntentFilter(
92                 UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED);
93         intentFilter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED);
94         mContext.registerReceiver(mBroadcastReceiver, intentFilter);
95         Log.i(this, "Registering car mode receiver: %s", intentFilter);
96 
97         mIsCarMode = getSystemCarMode();
98     }
99 
addListener(SystemStateListener listener)100     public void addListener(SystemStateListener listener) {
101         if (listener != null) {
102             mListeners.add(listener);
103         }
104     }
105 
removeListener(SystemStateListener listener)106     public boolean removeListener(SystemStateListener listener) {
107         return mListeners.remove(listener);
108     }
109 
isCarMode()110     public boolean isCarMode() {
111         return mIsCarMode;
112     }
113 
isDeviceAtEar()114     public boolean isDeviceAtEar() {
115         return isDeviceAtEar(mContext);
116     }
117 
118     /**
119      * Returns a guess whether the phone is up to the user's ear. Use the proximity sensor and
120      * the gravity sensor to make a guess
121      * @return true if the proximity sensor is activated, the magnitude of gravity in directions
122      *         parallel to the screen is greater than some configurable threshold, and the
123      *         y-component of gravity isn't less than some other configurable threshold.
124      */
isDeviceAtEar(Context context)125     public static boolean isDeviceAtEar(Context context) {
126         SensorManager sm = context.getSystemService(SensorManager.class);
127         if (sm == null) {
128             return false;
129         }
130         Sensor grav = sm.getDefaultSensor(Sensor.TYPE_GRAVITY);
131         Sensor proximity = sm.getDefaultSensor(Sensor.TYPE_PROXIMITY);
132         if (grav == null || proximity == null) {
133             return false;
134         }
135 
136         AtomicBoolean result = new AtomicBoolean(true);
137         CountDownLatch gravLatch = new CountDownLatch(1);
138         CountDownLatch proxLatch = new CountDownLatch(1);
139 
140         final double xyGravityThreshold = context.getResources().getFloat(
141                 R.dimen.device_on_ear_xy_gravity_threshold);
142         final double yGravityNegativeThreshold = context.getResources().getFloat(
143                 R.dimen.device_on_ear_y_gravity_negative_threshold);
144 
145         SensorEventListener listener = new SensorEventListener() {
146             @Override
147             public void onSensorChanged(SensorEvent event) {
148                 if (event.sensor.getType() == Sensor.TYPE_GRAVITY) {
149                     if (gravLatch.getCount() == 0) {
150                         return;
151                     }
152                     double xyMag = Math.sqrt(event.values[0] * event.values[0]
153                             + event.values[1] * event.values[1]);
154                     if (xyMag < xyGravityThreshold
155                             || event.values[1] < yGravityNegativeThreshold) {
156                         result.set(false);
157                     }
158                     gravLatch.countDown();
159                 } else if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) {
160                     if (proxLatch.getCount() == 0) {
161                         return;
162                     }
163                     if (event.values[0] >= proximity.getMaximumRange()) {
164                         result.set(false);
165                     }
166                     proxLatch.countDown();
167                 }
168             }
169 
170             @Override
171             public void onAccuracyChanged(Sensor sensor, int accuracy) {
172             }
173         };
174 
175         try {
176             sm.registerListener(listener, grav, SensorManager.SENSOR_DELAY_FASTEST);
177             sm.registerListener(listener, proximity, SensorManager.SENSOR_DELAY_FASTEST);
178             boolean accelValid = gravLatch.await(100, TimeUnit.MILLISECONDS);
179             boolean proxValid = proxLatch.await(100, TimeUnit.MILLISECONDS);
180             if (accelValid && proxValid) {
181                 return result.get();
182             } else {
183                 Log.w(SystemStateHelper.class.getSimpleName(),
184                         "Timed out waiting for sensors: %b %b", accelValid, proxValid);
185                 return false;
186             }
187         } catch (InterruptedException e) {
188             return false;
189         } finally {
190             sm.unregisterListener(listener);
191         }
192     }
193 
onEnterCarMode(int priority, String packageName)194     private void onEnterCarMode(int priority, String packageName) {
195         Log.i(this, "Entering carmode");
196         mIsCarMode = getSystemCarMode();
197         for (SystemStateListener listener : mListeners) {
198             listener.onCarModeChanged(priority, packageName, true /* isCarMode */);
199         }
200     }
201 
onExitCarMode(int priority, String packageName)202     private void onExitCarMode(int priority, String packageName) {
203         Log.i(this, "Exiting carmode");
204         mIsCarMode = getSystemCarMode();
205         for (SystemStateListener listener : mListeners) {
206             listener.onCarModeChanged(priority, packageName, false /* isCarMode */);
207         }
208     }
209 
210     /**
211      * Checks the system for the current car mode.
212      *
213      * @return True if in car mode, false otherwise.
214      */
getSystemCarMode()215     private boolean getSystemCarMode() {
216         UiModeManager uiModeManager =
217                 (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
218 
219         if (uiModeManager != null) {
220             return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
221         }
222 
223         return false;
224     }
225 }
226