1 /*
2  * Copyright (C) 2018 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.deviceidle;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothManager;
22 import android.bluetooth.BluetoothProfile;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.os.Handler;
28 import android.os.Message;
29 
30 import com.android.internal.annotations.GuardedBy;
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.server.DeviceIdleInternal;
33 
34 // TODO: Should we part of the apex, or the platform??
35 
36 /**
37  * Track whether there are any active Bluetooth devices connected.
38  */
39 public class BluetoothConstraint implements IDeviceIdleConstraint {
40     private static final String TAG = BluetoothConstraint.class.getSimpleName();
41     private static final long INACTIVITY_TIMEOUT_MS = 20 * 60 * 1000L;
42 
43     private final Context mContext;
44     private final Handler mHandler;
45     private final DeviceIdleInternal mLocalService;
46     private final BluetoothManager mBluetoothManager;
47 
48     private volatile boolean mConnected = true;
49     private volatile boolean mMonitoring = false;
50 
BluetoothConstraint( Context context, Handler handler, DeviceIdleInternal localService)51     public BluetoothConstraint(
52             Context context, Handler handler, DeviceIdleInternal localService) {
53         mContext = context;
54         mHandler = handler;
55         mLocalService = localService;
56         mBluetoothManager = mContext.getSystemService(BluetoothManager.class);
57     }
58 
59     @Override
startMonitoring()60     public synchronized void startMonitoring() {
61         // Start by assuming we have a connected bluetooth device.
62         mConnected = true;
63         mMonitoring = true;
64 
65         // Register a receiver to get updates on bluetooth devices disconnecting or the
66         // adapter state changing.
67         IntentFilter filter = new IntentFilter();
68         filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
69         filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
70         filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
71         mContext.registerReceiver(mReceiver, filter);
72 
73         // Some devices will try to stay connected indefinitely. Set a timeout to ignore them.
74         mHandler.sendMessageDelayed(
75                 Message.obtain(mHandler, mTimeoutCallback), INACTIVITY_TIMEOUT_MS);
76 
77         // Now we have the receiver registered, make a direct check for connected devices.
78         updateAndReportActiveLocked();
79     }
80 
81     @Override
stopMonitoring()82     public synchronized void stopMonitoring() {
83         mContext.unregisterReceiver(mReceiver);
84         mHandler.removeCallbacks(mTimeoutCallback);
85         mMonitoring = false;
86     }
87 
cancelMonitoringDueToTimeout()88     private synchronized void cancelMonitoringDueToTimeout() {
89         if (mMonitoring) {
90             mMonitoring = false;
91             mLocalService.onConstraintStateChanged(this, /* active= */ false);
92         }
93     }
94 
95     /**
96      * Check the latest data from BluetoothManager and let DeviceIdleController know whether we
97      * have connected devices (for example TV remotes / gamepads) and thus want to stay awake.
98      */
99     @GuardedBy("this")
updateAndReportActiveLocked()100     private void updateAndReportActiveLocked() {
101         final boolean connected = isBluetoothConnected(mBluetoothManager);
102         if (connected != mConnected) {
103             mConnected = connected;
104             // If we lost all of our connections, we are on track to going into idle state.
105             mLocalService.onConstraintStateChanged(this, /* active= */ mConnected);
106         }
107     }
108 
109     /**
110      * True if the bluetooth adapter exists, is enabled, and has at least one GATT device connected.
111      */
112     @VisibleForTesting
isBluetoothConnected(BluetoothManager bluetoothManager)113     static boolean isBluetoothConnected(BluetoothManager bluetoothManager) {
114         BluetoothAdapter adapter = bluetoothManager.getAdapter();
115         if (adapter != null && adapter.isEnabled()) {
116             return bluetoothManager.getConnectedDevices(BluetoothProfile.GATT).size() > 0;
117         }
118         return false;
119     }
120 
121     /**
122      * Registered in {@link #startMonitoring()}, unregistered in {@link #stopMonitoring()}.
123      */
124     @VisibleForTesting
125     final BroadcastReceiver mReceiver = new BroadcastReceiver() {
126         @Override
127         public void onReceive(Context context, Intent intent) {
128             if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(intent.getAction())) {
129                 mLocalService.exitIdle("bluetooth");
130             } else {
131                 updateAndReportActiveLocked();
132             }
133         }
134     };
135 
136     private final Runnable mTimeoutCallback = () -> cancelMonitoringDueToTimeout();
137 }
138