• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 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.libraries.testing.deviceshadower.internal.bluetooth;
18 
19 import static org.robolectric.util.ReflectionHelpers.callConstructor;
20 
21 import android.Manifest.permission;
22 import android.bluetooth.BluetoothAdapter;
23 import android.bluetooth.BluetoothClass;
24 import android.bluetooth.BluetoothDevice;
25 import android.bluetooth.BluetoothProfile;
26 import android.bluetooth.IBluetoothManager;
27 import android.content.AttributionSource;
28 import android.content.Intent;
29 import android.os.Build.VERSION;
30 import android.os.ParcelUuid;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.libraries.testing.deviceshadower.Bluelet;
34 import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.Event;
35 import com.android.libraries.testing.deviceshadower.internal.bluetooth.AdapterDelegate.State;
36 import com.android.libraries.testing.deviceshadower.internal.bluetooth.connection.RfcommDelegate;
37 import com.android.libraries.testing.deviceshadower.internal.common.BroadcastManager;
38 import com.android.libraries.testing.deviceshadower.internal.common.Interrupter;
39 import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
40 
41 import com.google.common.base.Preconditions;
42 import com.google.common.collect.ImmutableMap;
43 
44 import org.robolectric.util.ReflectionHelpers.ClassParameter;
45 
46 import java.util.Arrays;
47 import java.util.HashMap;
48 import java.util.HashSet;
49 import java.util.Map;
50 import java.util.Set;
51 
52 /**
53  * A container class of a real-world Bluetooth device.
54  */
55 public class BlueletImpl implements Bluelet {
56 
57     enum PairingConfirmation {
58         UNKNOWN,
59         CONFIRMED,
60         DENIED
61     }
62 
63     /**
64      * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}.
65      */
66     static final int REASON_SUCCESS = 0;
67     /**
68      * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}.
69      */
70     static final int UNBOND_REASON_AUTH_FAILED = 1;
71     /**
72      * See hidden {@link #EXTRA_REASON} and reason values in {@link BluetoothDevice}.
73      */
74     static final int UNBOND_REASON_AUTH_CANCELED = 3;
75 
76     /**
77      * Hidden in {@link BluetoothDevice}.
78      */
79     private static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON";
80 
81     private static final Logger LOGGER = Logger.create("BlueletImpl");
82 
83     private static final ImmutableMap<Integer, Integer> PROFILE_STATE_TO_ADAPTER_STATE =
84             ImmutableMap.<Integer, Integer>builder()
85                     .put(BluetoothProfile.STATE_CONNECTED, BluetoothAdapter.STATE_CONNECTED)
86                     .put(BluetoothProfile.STATE_CONNECTING, BluetoothAdapter.STATE_CONNECTING)
87                     .put(BluetoothProfile.STATE_DISCONNECTING, BluetoothAdapter.STATE_DISCONNECTING)
88                     .put(BluetoothProfile.STATE_DISCONNECTED, BluetoothAdapter.STATE_DISCONNECTED)
89                     .build();
90 
reset()91     public static void reset() {
92         RfcommDelegate.reset();
93     }
94 
95     public final String address;
96     String mName;
97     ParcelUuid[] mProfileUuids = new ParcelUuid[0];
98     int mPhonebookAccessPermission;
99     int mMessageAccessPermission;
100     int mSimAccessPermission;
101     final BluetoothAdapter mAdapter;
102     int mPassKey;
103 
104     private CreateBondOutcome mCreateBondOutcome = CreateBondOutcome.SUCCESS;
105     private int mCreateBondFailureReason;
106     private IoCapabilities mIoCapabilities = IoCapabilities.NO_INPUT_NO_OUTPUT;
107     private boolean mRefuseConnections;
108     private FetchUuidsTiming mFetchUuidsTiming = FetchUuidsTiming.AFTER_BONDING;
109     private boolean mEnableCVE20192225;
110 
111     private final Interrupter mInterrupter;
112     private final AdapterDelegate mAdapterDelegate;
113     private final RfcommDelegate mRfcommDelegate;
114     private final GattDelegate mGattDelegate;
115     private final BluetoothBroadcastHandler mBluetoothBroadcastHandler;
116     private final Map<String, Integer> mRemoteAddressToBondState = new HashMap<>();
117     private final Map<String, PairingConfirmation> mRemoteAddressToPairingConfirmation =
118             new HashMap<>();
119     private final Map<Integer, Integer> mProfileTypeToConnectionState = new HashMap<>();
120     private final Set<BluetoothDevice> mBondedDevices = new HashSet<>();
121 
BlueletImpl(String address, BroadcastManager broadcastManager)122     public BlueletImpl(String address, BroadcastManager broadcastManager) {
123         this.address = address;
124         this.mName = address;
125         this.mAdapter = callConstructor(BluetoothAdapter.class,
126                 ClassParameter.from(IBluetoothManager.class, new IBluetoothManagerImpl()),
127                 ClassParameter.from(AttributionSource.class,
128                         AttributionSource.myAttributionSource()));
129         mBluetoothBroadcastHandler = new BluetoothBroadcastHandler(broadcastManager);
130         mInterrupter = new Interrupter();
131         mAdapterDelegate = new AdapterDelegate(address, mBluetoothBroadcastHandler);
132         mRfcommDelegate = new RfcommDelegate(address, mBluetoothBroadcastHandler, mInterrupter);
133         mGattDelegate = new GattDelegate(address);
134     }
135 
136     @Override
setAdapterInitialState(int state)137     public Bluelet setAdapterInitialState(int state) throws IllegalArgumentException {
138         LOGGER.d(String.format("Address: %s, setAdapterInitialState(%d)", address, state));
139         Preconditions.checkArgument(
140                 state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_ON,
141                 "State must be BluetoothAdapter.STATE_ON or BluetoothAdapter.STATE_OFF.");
142         mAdapterDelegate.setState(State.lookup(state));
143         return this;
144     }
145 
146     @Override
setBluetoothClass(int bluetoothClass)147     public Bluelet setBluetoothClass(int bluetoothClass) {
148         mAdapterDelegate.setBluetoothClass(bluetoothClass);
149         return this;
150     }
151 
152     @Override
setScanMode(int scanMode)153     public Bluelet setScanMode(int scanMode) {
154         mAdapterDelegate.setScanMode(scanMode);
155         return this;
156     }
157 
158     @Override
setProfileUuids(ParcelUuid... profileUuids)159     public Bluelet setProfileUuids(ParcelUuid... profileUuids) {
160         this.mProfileUuids = profileUuids;
161         return this;
162     }
163 
164     @Override
setIoCapabilities(IoCapabilities ioCapabilities)165     public Bluelet setIoCapabilities(IoCapabilities ioCapabilities) {
166         this.mIoCapabilities = ioCapabilities;
167         return this;
168     }
169 
170     @Override
setCreateBondOutcome(CreateBondOutcome outcome, int failureReason)171     public Bluelet setCreateBondOutcome(CreateBondOutcome outcome, int failureReason) {
172         mCreateBondOutcome = outcome;
173         mCreateBondFailureReason = failureReason;
174         return this;
175     }
176 
177     @Override
setRefuseConnections(boolean refuse)178     public Bluelet setRefuseConnections(boolean refuse) {
179         mRefuseConnections = refuse;
180         return this;
181     }
182 
183     @Override
setRefuseGattConnections(boolean refuse)184     public Bluelet setRefuseGattConnections(boolean refuse) {
185         getGattDelegate().setRefuseConnections(refuse);
186         return this;
187     }
188 
189     @Override
setFetchUuidsTiming(FetchUuidsTiming fetchUuidsTiming)190     public Bluelet setFetchUuidsTiming(FetchUuidsTiming fetchUuidsTiming) {
191         this.mFetchUuidsTiming = fetchUuidsTiming;
192         return this;
193     }
194 
195     @Override
addBondedDevice(String address)196     public Bluelet addBondedDevice(String address) {
197         this.mBondedDevices.add(mAdapter.getRemoteDevice(address));
198         return this;
199     }
200 
201     @Override
enableCVE20192225(boolean value)202     public Bluelet enableCVE20192225(boolean value) {
203         this.mEnableCVE20192225 = value;
204         return this;
205     }
206 
getIoCapabilities()207     IoCapabilities getIoCapabilities() {
208         return mIoCapabilities;
209     }
210 
getCreateBondOutcome()211     CreateBondOutcome getCreateBondOutcome() {
212         return mCreateBondOutcome;
213     }
214 
getCreateBondFailureReason()215     int getCreateBondFailureReason() {
216         return mCreateBondFailureReason;
217     }
218 
getRefuseConnections()219     public boolean getRefuseConnections() {
220         return mRefuseConnections;
221     }
222 
getFetchUuidsTiming()223     public FetchUuidsTiming getFetchUuidsTiming() {
224         return mFetchUuidsTiming;
225     }
226 
getBondedDevices()227     BluetoothDevice[] getBondedDevices() {
228         return mBondedDevices.toArray(new BluetoothDevice[0]);
229     }
230 
getEnableCVE20192225()231     public boolean getEnableCVE20192225() {
232         return mEnableCVE20192225;
233     }
234 
enableAdapter()235     public void enableAdapter() {
236         LOGGER.d(String.format("Address: %s, enableAdapter()", address));
237         // TODO(b/200231384): async enabling, configurable delay, failure path
238         if (VERSION.SDK_INT < 23) {
239             mAdapterDelegate.processEvent(Event.USER_TURN_ON);
240             mAdapterDelegate.processEvent(Event.BREDR_STARTED);
241         } else {
242             mAdapterDelegate.processEvent(Event.BLE_TURN_ON);
243             mAdapterDelegate.processEvent(Event.BLE_STARTED);
244             mAdapterDelegate.processEvent(Event.USER_TURN_ON);
245             mAdapterDelegate.processEvent(Event.BREDR_STARTED);
246         }
247     }
248 
disableAdapter()249     public void disableAdapter() {
250         LOGGER.d(String.format("Address: %s, disableAdapter()", address));
251         // TODO(b/200231384): async disabling, configurable delay, failure path
252         if (VERSION.SDK_INT < 23) {
253             mAdapterDelegate.processEvent(Event.USER_TURN_OFF);
254             mAdapterDelegate.processEvent(Event.BREDR_STOPPED);
255         } else {
256             mAdapterDelegate.processEvent(Event.BLE_TURN_OFF);
257             mAdapterDelegate.processEvent(Event.BREDR_STOPPED);
258             mAdapterDelegate.processEvent(Event.USER_TURN_OFF);
259             mAdapterDelegate.processEvent(Event.BLE_STOPPED);
260         }
261     }
262 
getAdapterDelegate()263     public AdapterDelegate getAdapterDelegate() {
264         return mAdapterDelegate;
265     }
266 
getRfcommDelegate()267     public RfcommDelegate getRfcommDelegate() {
268         return mRfcommDelegate;
269     }
270 
getGattDelegate()271     public GattDelegate getGattDelegate() {
272         return mGattDelegate;
273     }
274 
getAdapter()275     public BluetoothAdapter getAdapter() {
276         return mAdapter;
277     }
278 
setInterruptible(int identifier)279     public void setInterruptible(int identifier) {
280         LOGGER.d(String.format("Address: %s, setInterruptible(%d)", address, identifier));
281         mInterrupter.setInterruptible(identifier);
282     }
283 
interrupt(int identifier)284     public void interrupt(int identifier) {
285         LOGGER.d(String.format("Address: %s, interrupt(%d)", address, identifier));
286         mInterrupter.interrupt(identifier);
287     }
288 
289     @VisibleForTesting
setAdapterState(int state)290     public void setAdapterState(int state) throws IllegalArgumentException {
291         State s = State.lookup(state);
292         if (s == null) {
293             throw new IllegalArgumentException();
294         }
295         mAdapterDelegate.setState(s);
296     }
297 
getBondState(String remoteAddress)298     public int getBondState(String remoteAddress) {
299         return mRemoteAddressToBondState.containsKey(remoteAddress)
300                 ? mRemoteAddressToBondState.get(remoteAddress)
301                 : BluetoothDevice.BOND_NONE;
302     }
303 
setBondState(String remoteAddress, int bondState, int failureReason)304     public void setBondState(String remoteAddress, int bondState, int failureReason) {
305         Intent intent =
306                 newDeviceIntent(BluetoothDevice.ACTION_BOND_STATE_CHANGED, remoteAddress)
307                         .putExtra(BluetoothDevice.EXTRA_BOND_STATE, bondState)
308                         .putExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE,
309                                 getBondState(remoteAddress));
310 
311         if (failureReason != REASON_SUCCESS) {
312             intent.putExtra(EXTRA_REASON, failureReason);
313         }
314 
315         LOGGER.d(
316                 String.format(
317                         "Address: %s, Bluetooth Bond State Change Intent: remote=%s, %s -> %s "
318                                 + "(reason=%s)",
319                         address, remoteAddress, getBondState(remoteAddress), bondState,
320                         failureReason));
321         mRemoteAddressToBondState.put(remoteAddress, bondState);
322         mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(
323                 intent, android.Manifest.permission.BLUETOOTH);
324     }
325 
onPairingRequest(String remoteAddress, int variant, int key)326     public void onPairingRequest(String remoteAddress, int variant, int key) {
327         Intent intent =
328                 newDeviceIntent(BluetoothDevice.ACTION_PAIRING_REQUEST, remoteAddress)
329                         .putExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, variant)
330                         .putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, key);
331 
332         LOGGER.d(
333                 String.format(
334                         "Address: %s, Bluetooth Pairing Request Intent: remote=%s, variant=%s, "
335                                 + "key=%s", address, remoteAddress, variant, key));
336         mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(intent, permission.BLUETOOTH);
337     }
338 
getPairingConfirmation(String remoteAddress)339     public PairingConfirmation getPairingConfirmation(String remoteAddress) {
340         PairingConfirmation confirmation = mRemoteAddressToPairingConfirmation.get(remoteAddress);
341         return confirmation == null ? PairingConfirmation.UNKNOWN : confirmation;
342     }
343 
setPairingConfirmation(String remoteAddress, PairingConfirmation confirmation)344     public void setPairingConfirmation(String remoteAddress, PairingConfirmation confirmation) {
345         mRemoteAddressToPairingConfirmation.put(remoteAddress, confirmation);
346     }
347 
onFetchedUuids(String remoteAddress, ParcelUuid[] profileUuids)348     public void onFetchedUuids(String remoteAddress, ParcelUuid[] profileUuids) {
349         Intent intent =
350                 newDeviceIntent(BluetoothDevice.ACTION_UUID, remoteAddress)
351                         .putExtra(BluetoothDevice.EXTRA_UUID, profileUuids);
352 
353         LOGGER.d(
354                 String.format(
355                         "Address: %s, Bluetooth Found UUIDs Intent: remoteAddress=%s, uuids=%s",
356                         address, remoteAddress, Arrays.toString(profileUuids)));
357         mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(
358                 intent, android.Manifest.permission.BLUETOOTH);
359     }
360 
maxProfileState(int a, int b)361     private static int maxProfileState(int a, int b) {
362         // Prefer connected > connecting > disconnecting > disconnected.
363         switch (a) {
364             case BluetoothProfile.STATE_CONNECTED:
365                 return a;
366             case BluetoothProfile.STATE_CONNECTING:
367                 return b == BluetoothProfile.STATE_CONNECTED ? b : a;
368             case BluetoothProfile.STATE_DISCONNECTING:
369                 return b == BluetoothProfile.STATE_CONNECTED
370                         || b == BluetoothProfile.STATE_CONNECTING
371                         ? b
372                         : a;
373             case BluetoothProfile.STATE_DISCONNECTED:
374             default:
375                 return b;
376         }
377     }
378 
getAdapterConnectionState()379     public int getAdapterConnectionState() {
380         int maxState = BluetoothProfile.STATE_DISCONNECTED;
381         for (int state : mProfileTypeToConnectionState.values()) {
382             maxState = maxProfileState(maxState, state);
383         }
384         return PROFILE_STATE_TO_ADAPTER_STATE.get(maxState);
385     }
386 
getProfileConnectionState(int profileType)387     public int getProfileConnectionState(int profileType) {
388         return mProfileTypeToConnectionState.containsKey(profileType)
389                 ? mProfileTypeToConnectionState.get(profileType)
390                 : BluetoothProfile.STATE_DISCONNECTED;
391     }
392 
setProfileConnectionState(int profileType, int state, String remoteAddress)393     public void setProfileConnectionState(int profileType, int state, String remoteAddress) {
394         int previousAdapterState = getAdapterConnectionState();
395         mProfileTypeToConnectionState.put(profileType, state);
396         int adapterState = getAdapterConnectionState();
397         if (previousAdapterState != adapterState) {
398             Intent intent =
399                     newDeviceIntent(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED, remoteAddress)
400                             .putExtra(BluetoothAdapter.EXTRA_PREVIOUS_CONNECTION_STATE,
401                                     previousAdapterState)
402                             .putExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, adapterState);
403 
404             LOGGER.d(
405                     "Adapter Connection State Changed Intent: "
406                             + previousAdapterState
407                             + " -> "
408                             + adapterState);
409             mBluetoothBroadcastHandler.mBroadcastManager.sendBroadcast(
410                     intent, android.Manifest.permission.BLUETOOTH);
411         }
412     }
413 
414     static class BluetoothBroadcastHandler implements AdapterDelegate.Callback,
415             RfcommDelegate.Callback {
416 
417         private final BroadcastManager mBroadcastManager;
418 
BluetoothBroadcastHandler(BroadcastManager broadcastManager)419         BluetoothBroadcastHandler(BroadcastManager broadcastManager) {
420             this.mBroadcastManager = broadcastManager;
421         }
422 
423         @Override
onAdapterStateChange(State prevState, State newState)424         public void onAdapterStateChange(State prevState, State newState) {
425             int prev = prevState.getValue();
426             int cur = newState.getValue();
427             LOGGER.d("Bluetooth State Change Intent: " + State.lookup(prev) + " -> " + State.lookup(
428                     cur));
429             Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED);
430             intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prev);
431             intent.putExtra(BluetoothAdapter.EXTRA_STATE, cur);
432             mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
433         }
434 
435         @Override
onBleStateChange(State prevState, State newState)436         public void onBleStateChange(State prevState, State newState) {
437             int prev = prevState.getValue();
438             int cur = newState.getValue();
439             LOGGER.d("BLE State Change Intent: " + State.lookup(prev) + " -> " + State.lookup(cur));
440             Intent intent = new Intent(BluetoothConstants.ACTION_BLE_STATE_CHANGED);
441             intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prev);
442             intent.putExtra(BluetoothAdapter.EXTRA_STATE, cur);
443             mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
444         }
445 
446         @Override
onConnectionStateChange(String remoteAddress, boolean isConnected)447         public void onConnectionStateChange(String remoteAddress, boolean isConnected) {
448             LOGGER.d("Bluetooth Connection State Change Intent, isConnected: " + isConnected);
449             Intent intent =
450                     isConnected
451                             ? newDeviceIntent(BluetoothDevice.ACTION_ACL_CONNECTED, remoteAddress)
452                             : newDeviceIntent(BluetoothDevice.ACTION_ACL_DISCONNECTED,
453                                     remoteAddress);
454             mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
455         }
456 
457         @Override
onDiscoveryStarted()458         public void onDiscoveryStarted() {
459             LOGGER.d("Bluetooth discovery started.");
460             Intent intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
461             mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
462         }
463 
464         @Override
onDiscoveryFinished()465         public void onDiscoveryFinished() {
466             LOGGER.d("Bluetooth discovery finished.");
467             Intent intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
468             mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
469         }
470 
471         @Override
onDeviceFound(String address, int bluetoothClass, String name)472         public void onDeviceFound(String address, int bluetoothClass, String name) {
473             LOGGER.d("Bluetooth device found, address: " + address);
474             Intent intent =
475                     newDeviceIntent(BluetoothDevice.ACTION_FOUND, address)
476                             .putExtra(
477                                     BluetoothDevice.EXTRA_CLASS,
478                                     callConstructor(
479                                             BluetoothClass.class,
480                                             ClassParameter.from(int.class, bluetoothClass)))
481                             .putExtra(BluetoothDevice.EXTRA_NAME, name);
482             // TODO(b/200231384): support rssi
483             // TODO(b/200231384): send broadcast with additional ACCESS_COARSE_LOCATION permission
484             // once broadcast permission is implemented.
485             mBroadcastManager.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
486         }
487     }
488 
newDeviceIntent(String action, String address)489     private static Intent newDeviceIntent(String action, String address) {
490         return new Intent(action)
491                 .putExtra(
492                         BluetoothDevice.EXTRA_DEVICE,
493                         BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address));
494     }
495 }
496