1/*
2 * Copyright (C) 2016 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
17package com.android.systemui.doze;
18
19import android.annotation.AnyThread;
20import android.app.ActivityManager;
21import android.app.AlarmManager;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.database.ContentObserver;
25import android.hardware.Sensor;
26import android.hardware.SensorEvent;
27import android.hardware.SensorEventListener;
28import android.hardware.SensorManager;
29import android.hardware.TriggerEvent;
30import android.hardware.TriggerEventListener;
31import android.net.Uri;
32import android.os.Handler;
33import android.os.SystemClock;
34import android.os.UserHandle;
35import android.provider.Settings;
36import android.text.TextUtils;
37import android.util.Log;
38
39import com.android.internal.hardware.AmbientDisplayConfiguration;
40import com.android.internal.logging.MetricsLogger;
41import com.android.internal.logging.nano.MetricsProto;
42import com.android.systemui.statusbar.phone.DozeParameters;
43import com.android.systemui.util.AlarmTimeout;
44import com.android.systemui.util.wakelock.WakeLock;
45
46import java.io.PrintWriter;
47import java.util.List;
48import java.util.function.Consumer;
49
50public class DozeSensors {
51
52    private static final boolean DEBUG = DozeService.DEBUG;
53
54    private static final String TAG = "DozeSensors";
55
56    private final Context mContext;
57    private final AlarmManager mAlarmManager;
58    private final SensorManager mSensorManager;
59    private final TriggerSensor[] mSensors;
60    private final ContentResolver mResolver;
61    private final TriggerSensor mPickupSensor;
62    private final DozeParameters mDozeParameters;
63    private final AmbientDisplayConfiguration mConfig;
64    private final WakeLock mWakeLock;
65    private final Consumer<Boolean> mProxCallback;
66    private final Callback mCallback;
67
68    private final Handler mHandler = new Handler();
69    private final ProxSensor mProxSensor;
70
71
72    public DozeSensors(Context context, AlarmManager alarmManager, SensorManager sensorManager,
73            DozeParameters dozeParameters,
74            AmbientDisplayConfiguration config, WakeLock wakeLock, Callback callback,
75            Consumer<Boolean> proxCallback, AlwaysOnDisplayPolicy policy) {
76        mContext = context;
77        mAlarmManager = alarmManager;
78        mSensorManager = sensorManager;
79        mDozeParameters = dozeParameters;
80        mConfig = config;
81        mWakeLock = wakeLock;
82        mProxCallback = proxCallback;
83        mResolver = mContext.getContentResolver();
84
85        mSensors = new TriggerSensor[] {
86                new TriggerSensor(
87                        mSensorManager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION),
88                        null /* setting */,
89                        dozeParameters.getPulseOnSigMotion(),
90                        DozeLog.PULSE_REASON_SENSOR_SIGMOTION, false /* touchCoords */,
91                        false /* touchscreen */),
92                mPickupSensor = new TriggerSensor(
93                        mSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE),
94                        Settings.Secure.DOZE_PULSE_ON_PICK_UP,
95                        config.pulseOnPickupAvailable(),
96                        DozeLog.PULSE_REASON_SENSOR_PICKUP, false /* touchCoords */,
97                        false /* touchscreen */),
98                new TriggerSensor(
99                        findSensorWithType(config.doubleTapSensorType()),
100                        Settings.Secure.DOZE_PULSE_ON_DOUBLE_TAP,
101                        true /* configured */,
102                        DozeLog.PULSE_REASON_SENSOR_DOUBLE_TAP,
103                        dozeParameters.doubleTapReportsTouchCoordinates(),
104                        true /* touchscreen */),
105                new TriggerSensor(
106                        findSensorWithType(config.longPressSensorType()),
107                        Settings.Secure.DOZE_PULSE_ON_LONG_PRESS,
108                        false /* settingDef */,
109                        true /* configured */,
110                        DozeLog.PULSE_REASON_SENSOR_LONG_PRESS,
111                        true /* reports touch coordinates */,
112                        true /* touchscreen */),
113        };
114
115        mProxSensor = new ProxSensor(policy);
116        mCallback = callback;
117    }
118
119    private Sensor findSensorWithType(String type) {
120        return findSensorWithType(mSensorManager, type);
121    }
122
123    static Sensor findSensorWithType(SensorManager sensorManager, String type) {
124        if (TextUtils.isEmpty(type)) {
125            return null;
126        }
127        List<Sensor> sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
128        for (Sensor s : sensorList) {
129            if (type.equals(s.getStringType())) {
130                return s;
131            }
132        }
133        return null;
134    }
135
136    public void setListening(boolean listen) {
137        for (TriggerSensor s : mSensors) {
138            s.setListening(listen);
139            if (listen) {
140                s.registerSettingsObserver(mSettingsObserver);
141            }
142        }
143        if (!listen) {
144            mResolver.unregisterContentObserver(mSettingsObserver);
145        }
146    }
147
148    /** Set the listening state of only the sensors that require the touchscreen. */
149    public void setTouchscreenSensorsListening(boolean listening) {
150        for (TriggerSensor sensor : mSensors) {
151            if (sensor.mRequiresTouchscreen) {
152                sensor.setListening(listening);
153            }
154        }
155    }
156
157    public void reregisterAllSensors() {
158        for (TriggerSensor s : mSensors) {
159            s.setListening(false);
160        }
161        for (TriggerSensor s : mSensors) {
162            s.setListening(true);
163        }
164    }
165
166    public void onUserSwitched() {
167        for (TriggerSensor s : mSensors) {
168            s.updateListener();
169        }
170    }
171
172    public void setProxListening(boolean listen) {
173        mProxSensor.setRequested(listen);
174    }
175
176    private final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
177        @Override
178        public void onChange(boolean selfChange, Uri uri, int userId) {
179            if (userId != ActivityManager.getCurrentUser()) {
180                return;
181            }
182            for (TriggerSensor s : mSensors) {
183                s.updateListener();
184            }
185        }
186    };
187
188    public void setDisableSensorsInterferingWithProximity(boolean disable) {
189        mPickupSensor.setDisabled(disable);
190    }
191
192    /** Dump current state */
193    public void dump(PrintWriter pw) {
194        for (TriggerSensor s : mSensors) {
195            pw.print("Sensor: "); pw.println(s.toString());
196        }
197        pw.print("ProxSensor: "); pw.println(mProxSensor.toString());
198    }
199
200    /**
201     * @return true if prox is currently far, false if near or null if unknown.
202     */
203    public Boolean isProximityCurrentlyFar() {
204        return mProxSensor.mCurrentlyFar;
205    }
206
207    private class ProxSensor implements SensorEventListener {
208
209        boolean mRequested;
210        boolean mRegistered;
211        Boolean mCurrentlyFar;
212        long mLastNear;
213        final AlarmTimeout mCooldownTimer;
214        final AlwaysOnDisplayPolicy mPolicy;
215
216
217        public ProxSensor(AlwaysOnDisplayPolicy policy) {
218            mPolicy = policy;
219            mCooldownTimer = new AlarmTimeout(mAlarmManager, this::updateRegistered,
220                    "prox_cooldown", mHandler);
221        }
222
223        void setRequested(boolean requested) {
224            if (mRequested == requested) {
225                // Send an update even if we don't re-register.
226                mHandler.post(() -> {
227                    if (mCurrentlyFar != null) {
228                        mProxCallback.accept(mCurrentlyFar);
229                    }
230                });
231                return;
232            }
233            mRequested = requested;
234            updateRegistered();
235        }
236
237        private void updateRegistered() {
238            setRegistered(mRequested && !mCooldownTimer.isScheduled());
239        }
240
241        private void setRegistered(boolean register) {
242            if (mRegistered == register) {
243                return;
244            }
245            if (register) {
246                mRegistered = mSensorManager.registerListener(this,
247                        mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY),
248                        SensorManager.SENSOR_DELAY_NORMAL, mHandler);
249            } else {
250                mSensorManager.unregisterListener(this);
251                mRegistered = false;
252                mCurrentlyFar = null;
253            }
254        }
255
256        @Override
257        public void onSensorChanged(SensorEvent event) {
258            if (DEBUG) Log.d(TAG, "onSensorChanged " + event);
259
260            mCurrentlyFar = event.values[0] >= event.sensor.getMaximumRange();
261            mProxCallback.accept(mCurrentlyFar);
262
263            long now = SystemClock.elapsedRealtime();
264            if (mCurrentlyFar == null) {
265                // Sensor has been unregistered by the proxCallback. Do nothing.
266            } else if (!mCurrentlyFar) {
267                mLastNear = now;
268            } else if (mCurrentlyFar && now - mLastNear < mPolicy.proxCooldownTriggerMs) {
269                // If the last near was very recent, we might be using more power for prox
270                // wakeups than we're saving from turning of the screen. Instead, turn it off
271                // for a while.
272                mCooldownTimer.schedule(mPolicy.proxCooldownPeriodMs,
273                        AlarmTimeout.MODE_IGNORE_IF_SCHEDULED);
274                updateRegistered();
275            }
276        }
277
278        @Override
279        public void onAccuracyChanged(Sensor sensor, int accuracy) {
280        }
281
282        @Override
283        public String toString() {
284            return String.format("{registered=%s, requested=%s, coolingDown=%s, currentlyFar=%s}",
285                    mRegistered, mRequested, mCooldownTimer.isScheduled(), mCurrentlyFar);
286        }
287    }
288
289    private class TriggerSensor extends TriggerEventListener {
290        final Sensor mSensor;
291        final boolean mConfigured;
292        final int mPulseReason;
293        final String mSetting;
294        final boolean mReportsTouchCoordinates;
295        final boolean mSettingDefault;
296        final boolean mRequiresTouchscreen;
297
298        private boolean mRequested;
299        private boolean mRegistered;
300        private boolean mDisabled;
301
302        public TriggerSensor(Sensor sensor, String setting, boolean configured, int pulseReason,
303                boolean reportsTouchCoordinates, boolean requiresTouchscreen) {
304            this(sensor, setting, true /* settingDef */, configured, pulseReason,
305                    reportsTouchCoordinates, requiresTouchscreen);
306        }
307
308        public TriggerSensor(Sensor sensor, String setting, boolean settingDef,
309                boolean configured, int pulseReason, boolean reportsTouchCoordinates,
310                boolean requiresTouchscreen) {
311            mSensor = sensor;
312            mSetting = setting;
313            mSettingDefault = settingDef;
314            mConfigured = configured;
315            mPulseReason = pulseReason;
316            mReportsTouchCoordinates = reportsTouchCoordinates;
317            mRequiresTouchscreen = requiresTouchscreen;
318        }
319
320        public void setListening(boolean listen) {
321            if (mRequested == listen) return;
322            mRequested = listen;
323            updateListener();
324        }
325
326        public void setDisabled(boolean disabled) {
327            if (mDisabled == disabled) return;
328            mDisabled = disabled;
329            updateListener();
330        }
331
332        public void updateListener() {
333            if (!mConfigured || mSensor == null) return;
334            if (mRequested && !mDisabled && enabledBySetting() && !mRegistered) {
335                mRegistered = mSensorManager.requestTriggerSensor(this, mSensor);
336                if (DEBUG) Log.d(TAG, "requestTriggerSensor " + mRegistered);
337            } else if (mRegistered) {
338                final boolean rt = mSensorManager.cancelTriggerSensor(this, mSensor);
339                if (DEBUG) Log.d(TAG, "cancelTriggerSensor " + rt);
340                mRegistered = false;
341            }
342        }
343
344        private boolean enabledBySetting() {
345            if (TextUtils.isEmpty(mSetting)) {
346                return true;
347            }
348            return Settings.Secure.getIntForUser(mResolver, mSetting, mSettingDefault ? 1 : 0,
349                    UserHandle.USER_CURRENT) != 0;
350        }
351
352        @Override
353        public String toString() {
354            return new StringBuilder("{mRegistered=").append(mRegistered)
355                    .append(", mRequested=").append(mRequested)
356                    .append(", mDisabled=").append(mDisabled)
357                    .append(", mConfigured=").append(mConfigured)
358                    .append(", mSensor=").append(mSensor).append("}").toString();
359        }
360
361        @Override
362        @AnyThread
363        public void onTrigger(TriggerEvent event) {
364            DozeLog.traceSensor(mContext, mPulseReason);
365            mHandler.post(mWakeLock.wrap(() -> {
366                if (DEBUG) Log.d(TAG, "onTrigger: " + triggerEventToString(event));
367                boolean sensorPerformsProxCheck = false;
368                if (mSensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
369                    int subType = (int) event.values[0];
370                    MetricsLogger.action(
371                            mContext, MetricsProto.MetricsEvent.ACTION_AMBIENT_GESTURE,
372                            subType);
373                    sensorPerformsProxCheck =
374                            mDozeParameters.getPickupSubtypePerformsProxCheck(subType);
375                }
376
377                mRegistered = false;
378                float screenX = -1;
379                float screenY = -1;
380                if (mReportsTouchCoordinates && event.values.length >= 2) {
381                    screenX = event.values[0];
382                    screenY = event.values[1];
383                }
384                mCallback.onSensorPulse(mPulseReason, sensorPerformsProxCheck, screenX, screenY);
385                updateListener();  // reregister, this sensor only fires once
386            }));
387        }
388
389        public void registerSettingsObserver(ContentObserver settingsObserver) {
390            if (mConfigured && !TextUtils.isEmpty(mSetting)) {
391                mResolver.registerContentObserver(
392                        Settings.Secure.getUriFor(mSetting), false /* descendants */,
393                        mSettingsObserver, UserHandle.USER_ALL);
394            }
395        }
396
397        private String triggerEventToString(TriggerEvent event) {
398            if (event == null) return null;
399            final StringBuilder sb = new StringBuilder("TriggerEvent[")
400                    .append(event.timestamp).append(',')
401                    .append(event.sensor.getName());
402            if (event.values != null) {
403                for (int i = 0; i < event.values.length; i++) {
404                    sb.append(',').append(event.values[i]);
405                }
406            }
407            return sb.append(']').toString();
408        }
409    }
410
411    public interface Callback {
412
413        /**
414         * Called when a sensor requests a pulse
415         * @param pulseReason Requesting sensor, e.g. {@link DozeLog#PULSE_REASON_SENSOR_PICKUP}
416         * @param sensorPerformedProxCheck true if the sensor already checked for FAR proximity.
417         * @param screenX the location on the screen where the sensor fired or -1
418         *                if the sensor doesn't support reporting screen locations.
419         * @param screenY the location on the screen where the sensor fired or -1
420         *                if the sensor doesn't support reporting screen locations.
421         */
422        void onSensorPulse(int pulseReason, boolean sensorPerformedProxCheck,
423                float screenX, float screenY);
424    }
425}
426