1 /*
2  * Copyright (C) 2017 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.car;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothDevice;
21 import android.car.hardware.power.CarPowerManager;
22 import android.car.hardware.power.CarPowerManager.CarPowerStateListenerWithCompletion;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.os.UserHandle;
28 import android.provider.Settings;
29 import android.util.Log;
30 
31 import java.io.PrintWriter;
32 import java.util.Objects;
33 import java.util.concurrent.CompletableFuture;
34 
35 /**
36  * A Bluetooth Device Connection policy that is specific to the use cases of a Car. Contains policy
37  * for deciding when to trigger connection and disconnection events.
38  */
39 
40 public class BluetoothDeviceConnectionPolicy {
41     private static final String TAG = "BluetoothDeviceConnectionPolicy";
42     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
43 
44     private final int mUserId;
45     private final Context mContext;
46     private final BluetoothAdapter mBluetoothAdapter;
47     private final CarBluetoothService mCarBluetoothService;
48 
49     private CarPowerManager mCarPowerManager;
50     private final CarPowerStateListenerWithCompletion mCarPowerStateListener =
51             new CarPowerStateListenerWithCompletion() {
52         @Override
53         public void onStateChanged(int state, CompletableFuture<Void> future) {
54             logd("Car power state has changed to " + state);
55 
56             // ON is the state when user turned on the car (it can be either ignition or
57             // door unlock) the policy for ON is defined by OEMs and we can rely on that.
58             if (state == CarPowerManager.CarPowerStateListener.ON) {
59                 logd("Car is powering on. Enable Bluetooth and auto-connect to devices");
60                 if (isBluetoothPersistedOn()) {
61                     enableBluetooth();
62                 }
63 
64                 // The above isBluetoothPersistedOn() call is always true when the adapter is on and
65                 // can be true or false if the adapter is off. If we are turned the adapter back on
66                 // then this connectDevices() call would fail at first here but be caught by the
67                 // following adapter on broadcast below. We'll only do this if the adapter is on
68                 if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
69                     connectDevices();
70                 }
71                 return;
72             }
73 
74             // Since we're appearing to be off after shutdown prepare, but may stay on in idle mode,
75             // we'll turn off Bluetooth to disconnect devices and better the "off" illusion
76             if (state == CarPowerManager.CarPowerStateListener.SHUTDOWN_PREPARE) {
77                 logd("Car is preparing for shutdown. Disable bluetooth adapter");
78                 disableBluetooth();
79 
80                 // Let CPMS know we're ready to shutdown. Otherwise, CPMS will get stuck for
81                 // up to an hour.
82                 if (future != null) {
83                     future.complete(null);
84                 }
85                 return;
86             }
87         }
88     };
89 
90     /**
91      * Get the policy's CarPowerStateListenerWithCompletion object
92      *
93      * For testing purposes only
94      */
getCarPowerStateListener()95     public CarPowerStateListenerWithCompletion getCarPowerStateListener() {
96         return mCarPowerStateListener;
97     }
98 
99     /**
100      * A BroadcastReceiver that listens specifically for actions related to the profile we're
101      * tracking and uses them to update the status.
102      *
103      * On BluetoothAdapter.ACTION_STATE_CHANGED:
104      *    If the adapter is going into the ON state, then commit trigger auto connection.
105      */
106     private class BluetoothBroadcastReceiver extends BroadcastReceiver {
107         @Override
onReceive(Context context, Intent intent)108         public void onReceive(Context context, Intent intent) {
109             String action = intent.getAction();
110             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
111             if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
112                 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
113                 logd("Bluetooth Adapter state changed: " + Utils.getAdapterStateName(state));
114                 if (state == BluetoothAdapter.STATE_ON) {
115                     connectDevices();
116                 }
117             }
118         }
119     }
120     private final BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
121 
122     /**
123      * Create a new BluetoothDeviceConnectionPolicy object, responsible for encapsulating the
124      * default policy for when to initiate device connections given the list of prioritized devices
125      * for each profile.
126      *
127      * @param context - The context of the creating application
128      * @param userId - The user ID we're operating as
129      * @param bluetoothService - A reference to CarBluetoothService so we can connect devices
130      * @return A new instance of a BluetoothProfileDeviceManager, or null on any error
131      */
create(Context context, int userId, CarBluetoothService bluetoothService)132     public static BluetoothDeviceConnectionPolicy create(Context context, int userId,
133             CarBluetoothService bluetoothService) {
134         try {
135             return new BluetoothDeviceConnectionPolicy(context, userId, bluetoothService);
136         } catch (NullPointerException e) {
137             return null;
138         }
139     }
140 
141     /**
142      * Create a new BluetoothDeviceConnectionPolicy object, responsible for encapsulating the
143      * default policy for when to initiate device connections given the list of prioritized devices
144      * for each profile.
145      *
146      * @param context - The context of the creating application
147      * @param userId - The user ID we're operating as
148      * @param bluetoothService - A reference to CarBluetoothService so we can connect devices
149      * @return A new instance of a BluetoothProfileDeviceManager
150      */
BluetoothDeviceConnectionPolicy(Context context, int userId, CarBluetoothService bluetoothService)151     private BluetoothDeviceConnectionPolicy(Context context, int userId,
152             CarBluetoothService bluetoothService) {
153         mUserId = userId;
154         mContext = Objects.requireNonNull(context);
155         mCarBluetoothService = bluetoothService;
156         mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
157         mBluetoothAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter());
158     }
159 
160     /**
161      * Setup the Bluetooth profile service connections and Vehicle Event listeners.
162      * and start the state machine -{@link BluetoothAutoConnectStateMachine}
163      */
init()164     public void init() {
165         logd("init()");
166         IntentFilter profileFilter = new IntentFilter();
167         profileFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
168         mContext.registerReceiverAsUser(mBluetoothBroadcastReceiver, UserHandle.CURRENT,
169                 profileFilter, null, null);
170         mCarPowerManager = CarLocalServices.createCarPowerManager(mContext);
171         // CarLocalServices can fail to return a service.
172         if (mCarPowerManager != null) {
173             mCarPowerManager.setListenerWithCompletion(mCarPowerStateListener);
174         } else {
175             logd("Failed to get car power manager");
176         }
177 
178         // Since we do this only on start up and on user switch, it's safe to kick off a connect on
179         // init. If we have a connect in progress, this won't hurt anything. If we already have
180         // devices connected, this will add on top of it. We _could_ enter this from a crash
181         // recovery, but that would at worst cause more devices to connect and wouldn't change the
182         // existing devices.
183         if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
184             // CarPowerManager doesn't provide a getState() or that would go here too.
185             connectDevices();
186         }
187     }
188 
189     /**
190      * Clean up slate. Close the Bluetooth profile service connections and quit the state machine -
191      * {@link BluetoothAutoConnectStateMachine}
192      */
release()193     public void release() {
194         logd("release()");
195         if (mCarPowerManager != null) {
196             mCarPowerManager.clearListener();
197             mCarPowerManager = null;
198         }
199         if (mBluetoothBroadcastReceiver != null) {
200             mContext.unregisterReceiver(mBluetoothBroadcastReceiver);
201         }
202     }
203 
204     /**
205      * Tell each Profile device manager that its time to begin auto connecting devices
206      */
connectDevices()207     public void connectDevices() {
208         logd("Connect devices for each profile");
209         mCarBluetoothService.connectDevices();
210     }
211 
212     /**
213      * Get the persisted Bluetooth state from Settings
214      *
215      * @return True if the persisted Bluetooth state is on, false otherwise
216      */
isBluetoothPersistedOn()217     private boolean isBluetoothPersistedOn() {
218         return (Settings.Global.getInt(
219                 mContext.getContentResolver(), Settings.Global.BLUETOOTH_ON, -1) != 0);
220     }
221 
222     /**
223      * Turn on the Bluetooth Adapter.
224      */
enableBluetooth()225     private void enableBluetooth() {
226         logd("Enable bluetooth adapter");
227         if (mBluetoothAdapter == null) {
228             Log.e(TAG, "Cannot enable Bluetooth adapter. The object is null.");
229             return;
230         }
231         mBluetoothAdapter.enable();
232     }
233 
234     /**
235      * Turn off the Bluetooth Adapter.
236      *
237      * Tells BluetoothAdapter to shut down _without_ persisting the off state as the desired state
238      * of the Bluetooth adapter for next start up.
239      */
disableBluetooth()240     private void disableBluetooth() {
241         logd("Disable bluetooth, do not persist state across reboot");
242         if (mBluetoothAdapter == null) {
243             Log.e(TAG, "Cannot disable Bluetooth adapter. The object is null.");
244             return;
245         }
246         mBluetoothAdapter.disable(false);
247     }
248 
249     /**
250      * Print the verbose status of the object
251      */
dump(PrintWriter writer, String indent)252     public void dump(PrintWriter writer, String indent) {
253         writer.println(indent + TAG + ":");
254         writer.println(indent + "\tUserId: " + mUserId);
255     }
256 
257     /**
258      * Print to debug if debug is enabled
259      */
logd(String msg)260     private static void logd(String msg) {
261         if (DBG) {
262             Log.d(TAG, msg);
263         }
264     }
265 }
266