1 /*
2  * Copyright (C) 2019 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 static org.junit.Assert.assertTrue;
20 
21 import android.bluetooth.BluetoothAdapter;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.provider.Settings;
27 import android.util.Log;
28 
29 import androidx.test.InstrumentationRegistry;
30 
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.locks.Condition;
33 import java.util.concurrent.locks.ReentrantLock;
34 
35 /**
36  * Contains the utilities to aid in car Bluetooth testing
37  */
38 public class BluetoothAdapterHelper {
39     private static final String TAG = "BluetoothAdapterHelper";
40     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
41     private Context mContext;
42 
43     private BluetoothAdapterReceiver mBluetoothAdapterReceiver;
44     private BluetoothAdapter mBluetoothAdapter;
45 
46     // Timeout for waiting for an adapter state change
47     private static final int BT_ADAPTER_TIMEOUT_MS = 8000; // ms
48 
49     // Objects to block until the adapter has reached a desired state
50     private ReentrantLock mBluetoothAdapterLock;
51     private Condition mConditionAdapterStateReached;
52     private int mDesiredState;
53 
54     /**
55      * Handles BluetoothAdapter state changes and signals when we've reached a desired state
56      */
57     private class BluetoothAdapterReceiver extends BroadcastReceiver {
58         @Override
onReceive(Context context, Intent intent)59         public void onReceive(Context context, Intent intent) {
60 
61             // Decode the intent
62             String action = intent.getAction();
63 
64             // Watch for BluetoothAdapter intents only
65             if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
66                 int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
67                 logd("Bluetooth adapter state changed: " + newState);
68 
69                 // Signal if the state is set to the one we're waiting on. If its not and we got a
70                 // STATE_OFF event then handle the unexpected off event. Note that we could
71                 // proactively turn the adapter back on to continue testing. For now we'll just
72                 // log it
73                 mBluetoothAdapterLock.lock();
74                 try {
75                     if (mDesiredState == newState) {
76                         mConditionAdapterStateReached.signal();
77                     } else if (newState == BluetoothAdapter.STATE_OFF) {
78                         logw("Bluetooth turned off unexpectedly while test was running.");
79                     }
80                 } finally {
81                     mBluetoothAdapterLock.unlock();
82                 }
83             }
84         }
85     }
86 
87     /**
88      * Create a BluetoothAdapterHelper and tell it how to log
89      *
90      * @param tag - The logging tag you wish it to have
91      */
BluetoothAdapterHelper()92     public BluetoothAdapterHelper() {
93         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
94         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
95 
96         mDesiredState = -1; // Set and checked by waitForAdapterState()
97         mBluetoothAdapterLock = new ReentrantLock();
98         mConditionAdapterStateReached = mBluetoothAdapterLock.newCondition();
99         mBluetoothAdapterReceiver = new BluetoothAdapterReceiver();
100     }
101 
102     /**
103      * Setup the helper and begin receiving BluetoothAdapter events.
104      *
105      * Must be called before functions will work.
106      */
init()107     public void init() {
108         IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
109         mContext.registerReceiver(mBluetoothAdapterReceiver, filter);
110     }
111 
112     /**
113      * Release resource in preparation to destroy the object
114      */
release()115     public void release() {
116         mContext.unregisterReceiver(mBluetoothAdapterReceiver);
117     }
118 
119     /**
120      * Get the state of the Bluetooth Adapter
121      */
getAdapterState()122     public int getAdapterState() {
123         return mBluetoothAdapter.getState();
124     }
125 
126     /**
127      * Get the persisted Bluetooth state from Settings
128      *
129      * @return True if the persisted Bluetooth state is on, false otherwise
130      */
isAdapterPersistedOn()131     public boolean isAdapterPersistedOn() {
132         return (Settings.Global.getInt(
133                 mContext.getContentResolver(), Settings.Global.BLUETOOTH_ON, -1) != 0);
134     }
135 
136     /**
137      * Wait until the adapter is ON. If the adapter is already on it will return immediately.
138      */
waitForAdapterOn()139     public synchronized void waitForAdapterOn() {
140         waitForAdapterState(BluetoothAdapter.STATE_ON);
141     }
142 
143     /**
144      * Wait until the adapter is OFF. If the adapter is already off it will return immediately.
145      */
waitForAdapterOff()146     public synchronized void waitForAdapterOff() {
147         waitForAdapterState(BluetoothAdapter.STATE_OFF);
148     }
149 
150     /**
151      * Wait for the bluetooth adapter to be in a given state
152      */
waitForAdapterState(int desiredState)153     private synchronized void waitForAdapterState(int desiredState) {
154         logd("Waiting for adapter state " + Utils.getAdapterStateName(desiredState));
155         mBluetoothAdapterLock.lock();
156         try {
157             // Update the desired state so that we'll signal when we get there
158             mDesiredState = desiredState;
159 
160             // Wait until we're reached that desired state
161             while (desiredState != mBluetoothAdapter.getState()) {
162                 if (!mConditionAdapterStateReached.await(
163                         BT_ADAPTER_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
164                     loge("Timeout while waiting for Bluetooth adapter state "
165                             + Utils.getAdapterStateName(desiredState));
166                     break;
167                 }
168             }
169         } catch (InterruptedException e) {
170             logw("waitForAdapterState(" + Utils.getAdapterStateName(desiredState)
171                     + "): interrupted, Reason: " + e);
172         } finally {
173             mBluetoothAdapterLock.unlock();
174         }
175         assertTrue(mBluetoothAdapter.getState() == desiredState);
176     }
177 
178     /**
179      * Request that the adapter be turned on.
180      */
forceAdapterOn()181     public synchronized void forceAdapterOn() {
182         forceAdapterState(BluetoothAdapter.STATE_ON, true);
183     }
184 
185     /**
186      * Request that the adapter be turned off.
187      */
forceAdapterOff()188     public synchronized void forceAdapterOff() {
189         forceAdapterState(BluetoothAdapter.STATE_OFF, true);
190     }
191 
192     /**
193      * Request that the adapter be turned off. Do not persist the off state across a reboot
194      */
forceAdapterOffDoNotPersist()195     public synchronized void forceAdapterOffDoNotPersist() {
196         forceAdapterState(BluetoothAdapter.STATE_OFF, false);
197     }
198 
199     /**
200      * Request that the adapter be turned eother on or off.
201      */
forceAdapterState(int desiredState, boolean persistAcrossReboot)202     private void forceAdapterState(int desiredState, boolean persistAcrossReboot) {
203         logd("Forcing adapter to be " + Utils.getAdapterStateName(desiredState));
204         // If the current state matches the requested state, these calls return immediately and
205         // our loop below will simply read the proper state
206         if (desiredState == BluetoothAdapter.STATE_ON) {
207             mBluetoothAdapter.enable();
208         } else {
209             mBluetoothAdapter.disable(persistAcrossReboot);
210         }
211         waitForAdapterState(desiredState);
212     }
213 
214     /**
215      * Log a message to DEBUG if debug is enabled
216      */
logd(String msg)217     private void logd(String msg) {
218         if (DBG) {
219             Log.d(TAG, msg);
220         }
221     }
222 
223     /**
224      * Log a message to WARN
225      */
logw(String msg)226     private void logw(String msg) {
227         Log.w(TAG, msg);
228     }
229 
230     /**
231      * Log a message to ERROR
232      */
loge(String msg)233     private void loge(String msg) {
234         Log.e(TAG, msg);
235     }
236 }
237