• 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 android.annotation.Nullable;
20 import android.bluetooth.BluetoothAdapter;
21 import android.bluetooth.BluetoothGatt;
22 import android.bluetooth.IBluetoothGattCallback;
23 import android.bluetooth.IBluetoothGattServerCallback;
24 import android.bluetooth.le.AdvertiseData;
25 import android.bluetooth.le.AdvertiseSettings;
26 import android.bluetooth.le.ScanFilter;
27 import android.bluetooth.le.ScanRecord;
28 import android.bluetooth.le.ScanResult;
29 import android.bluetooth.le.ScanSettings;
30 import android.os.Build;
31 import android.os.Build.VERSION;
32 import android.os.ParcelUuid;
33 import android.os.SystemClock;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.libraries.testing.deviceshadower.internal.DeviceShadowEnvironmentImpl;
37 import com.android.libraries.testing.deviceshadower.internal.DeviceletImpl;
38 import com.android.libraries.testing.deviceshadower.internal.utils.GattHelper;
39 import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
40 
41 import com.google.common.base.Preconditions;
42 import com.google.common.primitives.Bytes;
43 
44 import org.robolectric.util.ReflectionHelpers;
45 import org.robolectric.util.ReflectionHelpers.ClassParameter;
46 
47 import java.util.ArrayList;
48 import java.util.Collection;
49 import java.util.HashMap;
50 import java.util.HashSet;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.Set;
54 import java.util.concurrent.ConcurrentHashMap;
55 import java.util.concurrent.Future;
56 import java.util.concurrent.atomic.AtomicBoolean;
57 import java.util.concurrent.atomic.AtomicInteger;
58 
59 /**
60  * Delegate to operate gatt operations.
61  */
62 public class GattDelegate {
63 
64     private static final int DEFAULT_RSSI = -50;
65     private static final Logger LOGGER = Logger.create("GattDelegate");
66 
67     // chipset properties
68     // use 2 as API 21 requires multi-advertisement support to use Le Advertising.
69     private final int mMaxAdvertiseInstances = 2;
70     private final AtomicBoolean mIsOffloadedFilteringSupported = new AtomicBoolean(false);
71     private final String mAddress;
72     private final AtomicInteger mCurrentClientIf = new AtomicInteger(0);
73     private final AtomicInteger mCurrentServerIf = new AtomicInteger(0);
74     private final AtomicBoolean mCurrentConnectionState = new AtomicBoolean(false);
75     private final Map<ParcelUuid, Service> mServices = new HashMap<>();
76     private final Map<Integer, IBluetoothGattCallback> mClientCallbacks;
77     private final Map<Integer, IBluetoothGattServerCallback> mServerCallbacks;
78     private final Map<Integer, Advertiser> mAdvertisers;
79     private final Map<Integer, Scanner> mScanners;
80     @Nullable
81     private Request mLastRequest;
82     private boolean mConnectable = true;
83 
84     /**
85      * The parameters of a request, e.g. readCharacteristic(). Subclass for each request.
86      *
87      * @see #getLastRequest()
88      */
89     abstract static class Request {
90 
91         final int mSrvcType;
92         final int mSrvcInstId;
93         final ParcelUuid mSrvcId;
94         final int mCharInstId;
95         final ParcelUuid mCharId;
96 
Request(int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId, ParcelUuid charId)97         Request(int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId,
98                 ParcelUuid charId) {
99             this.mSrvcType = srvcType;
100             this.mSrvcInstId = srvcInstId;
101             this.mSrvcId = srvcId;
102             this.mCharInstId = charInstId;
103             this.mCharId = charId;
104         }
105     }
106 
107     /**
108      * Corresponds to {@link android.bluetooth.IBluetoothGatt#readCharacteristic}.
109      */
110     static class ReadCharacteristicRequest extends Request {
111 
ReadCharacteristicRequest( int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId, ParcelUuid charId)112         ReadCharacteristicRequest(
113                 int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId,
114                 ParcelUuid charId) {
115             super(srvcType, srvcInstId, srvcId, charInstId, charId);
116         }
117     }
118 
119     /**
120      * Corresponds to {@link android.bluetooth.IBluetoothGatt#readDescriptor}.
121      */
122     static class ReadDescriptorRequest extends Request {
123 
124         final int mDescrInstId;
125         final ParcelUuid mDescrId;
126 
ReadDescriptorRequest( int srvcType, int srvcInstId, ParcelUuid srvcId, int charInstId, ParcelUuid charId, int descrInstId, ParcelUuid descrId)127         ReadDescriptorRequest(
128                 int srvcType,
129                 int srvcInstId,
130                 ParcelUuid srvcId,
131                 int charInstId,
132                 ParcelUuid charId,
133                 int descrInstId,
134                 ParcelUuid descrId) {
135             super(srvcType, srvcInstId, srvcId, charInstId, charId);
136             this.mDescrInstId = descrInstId;
137             this.mDescrId = descrId;
138         }
139     }
140 
GattDelegate(String address)141     GattDelegate(String address) {
142         this(
143                 address,
144                 new HashMap<>(),
145                 new HashMap<>(),
146                 new ConcurrentHashMap<>(),
147                 new ConcurrentHashMap<>());
148     }
149 
150     @VisibleForTesting
GattDelegate( String address, Map<Integer, IBluetoothGattCallback> clientCallbacks, Map<Integer, IBluetoothGattServerCallback> serverCallbacks, Map<Integer, Advertiser> advertisers, Map<Integer, Scanner> scanners)151     GattDelegate(
152             String address,
153             Map<Integer, IBluetoothGattCallback> clientCallbacks,
154             Map<Integer, IBluetoothGattServerCallback> serverCallbacks,
155             Map<Integer, Advertiser> advertisers,
156             Map<Integer, Scanner> scanners) {
157         this.mAddress = address;
158         this.mClientCallbacks = clientCallbacks;
159         this.mServerCallbacks = serverCallbacks;
160         this.mAdvertisers = advertisers;
161         this.mScanners = scanners;
162     }
163 
setRefuseConnections(boolean refuse)164     public void setRefuseConnections(boolean refuse) {
165         this.mConnectable = !refuse;
166     }
167 
168     /**
169      * Used to maintain state between the request (e.g. readCharacteristic()) and sendResponse().
170      */
171     @Nullable
getLastRequest()172     Request getLastRequest() {
173         return mLastRequest;
174     }
175 
176     /**
177      * @see #getLastRequest()
178      */
setLastRequest(@ullable Request params)179     void setLastRequest(@Nullable Request params) {
180         mLastRequest = params;
181     }
182 
getClientIf()183     public int getClientIf() {
184         // TODO(b/200231384): support multiple client if.
185         return mCurrentClientIf.get();
186     }
187 
getServerIf()188     public int getServerIf() {
189         // TODO(b/200231384): support multiple server if.
190         return mCurrentServerIf.get();
191     }
192 
getServerCallback(int serverIf)193     public IBluetoothGattServerCallback getServerCallback(int serverIf) {
194         return mServerCallbacks.get(serverIf);
195     }
196 
getClientCallback(int clientIf)197     public IBluetoothGattCallback getClientCallback(int clientIf) {
198         return mClientCallbacks.get(clientIf);
199     }
200 
registerServer(IBluetoothGattServerCallback callback)201     public int registerServer(IBluetoothGattServerCallback callback) {
202         mServerCallbacks.put(mCurrentServerIf.incrementAndGet(), callback);
203         return getServerIf();
204     }
205 
registerClient(IBluetoothGattCallback callback)206     public int registerClient(IBluetoothGattCallback callback) {
207         mClientCallbacks.put(mCurrentClientIf.incrementAndGet(), callback);
208         LOGGER.d(String.format("Client registered on %s, clientIf: %d", mAddress, getClientIf()));
209         return getClientIf();
210     }
211 
unregisterClient(int clientIf)212     public void unregisterClient(int clientIf) {
213         mClientCallbacks.remove(clientIf);
214         LOGGER.d(String.format("Client unregistered on %s, clientIf: %d", mAddress, clientIf));
215     }
216 
unregisterServer(int serverIf)217     public void unregisterServer(int serverIf) {
218         mServerCallbacks.remove(serverIf);
219     }
220 
getMaxAdvertiseInstances()221     public int getMaxAdvertiseInstances() {
222         return mMaxAdvertiseInstances;
223     }
224 
isOffloadedFilteringSupported()225     public boolean isOffloadedFilteringSupported() {
226         return mIsOffloadedFilteringSupported.get();
227     }
228 
connect(String address)229     public boolean connect(String address) {
230         return mConnectable;
231     }
232 
disconnect(String address)233     public boolean disconnect(String address) {
234         return true;
235     }
236 
clientConnectionStateChange( int state, int clientIf, boolean connected, String address)237     public void clientConnectionStateChange(
238             int state, int clientIf, boolean connected, String address) {
239         if (connected != mCurrentConnectionState.get()) {
240             mCurrentConnectionState.set(connected);
241             IBluetoothGattCallback callback = getClientCallback(clientIf);
242             if (callback != null) {
243                 callback.onClientConnectionState(state, clientIf, connected, address);
244             }
245         }
246     }
247 
serverConnectionStateChange( int state, int serverIf, boolean connected, String address)248     public void serverConnectionStateChange(
249             int state, int serverIf, boolean connected, String address) {
250         if (connected != mCurrentConnectionState.get()) {
251             mCurrentConnectionState.set(connected);
252             IBluetoothGattServerCallback callback = getServerCallback(serverIf);
253             if (callback != null) {
254                 callback.onServerConnectionState(state, serverIf, connected, address);
255             }
256         }
257     }
258 
addService(ParcelUuid uuid)259     public Service addService(ParcelUuid uuid) {
260         Service srvc = new Service(uuid);
261         mServices.put(uuid, srvc);
262         return srvc;
263     }
264 
getServices()265     public Collection<Service> getServices() {
266         return mServices.values();
267     }
268 
getService(ParcelUuid uuid)269     public Service getService(ParcelUuid uuid) {
270         return mServices.get(uuid);
271     }
272 
clientSetMtu(int clientIf, int mtu, String serverAddress)273     public void clientSetMtu(int clientIf, int mtu, String serverAddress) {
274         IBluetoothGattCallback callback = getClientCallback(clientIf);
275         if (callback != null && Build.VERSION.SDK_INT >= 21) {
276             callback.onConfigureMTU(serverAddress, mtu, BluetoothGatt.GATT_SUCCESS);
277         }
278     }
279 
serverSetMtu(int serverIf, int mtu, String clientAddress)280     public void serverSetMtu(int serverIf, int mtu, String clientAddress) {
281         IBluetoothGattServerCallback callback = getServerCallback(serverIf);
282         if (callback != null && Build.VERSION.SDK_INT >= 22) {
283             callback.onMtuChanged(clientAddress, mtu);
284         }
285     }
286 
startMultiAdvertising( int appIf, AdvertiseData advertiseData, AdvertiseData scanResponse, final AdvertiseSettings settings)287     public void startMultiAdvertising(
288             int appIf,
289             AdvertiseData advertiseData,
290             AdvertiseData scanResponse,
291             final AdvertiseSettings settings) {
292         LOGGER.d(String.format("startMultiAdvertising(%d) on %s", appIf, mAddress));
293         final Advertiser advertiser =
294                 new Advertiser(
295                         appIf,
296                         mAddress,
297                         DeviceShadowEnvironmentImpl.getLocalBlueletImpl().mName,
298                         txPowerFromFlag(settings.getTxPowerLevel()),
299                         advertiseData,
300                         scanResponse,
301                         settings);
302         mAdvertisers.put(appIf, advertiser);
303         final IBluetoothGattCallback callback = mClientCallbacks.get(appIf);
304         @SuppressWarnings("unused") // go/futurereturn-lsc
305         Future<?> possiblyIgnoredError =
306                 DeviceShadowEnvironmentImpl.run(
307                         mAddress,
308                         () -> {
309                             callback.onMultiAdvertiseCallback(
310                                     BluetoothConstants.ADVERTISE_SUCCESS, true /* isStart */,
311                                     settings);
312                             return null;
313                         });
314     }
315 
316     /**
317      * Returns TxPower in dBm as measured at the source.
318      *
319      * <p>Note that this will vary by device and the values are only roughly accurate. The
320      * measurements were taken with a Nexus 6. Copied from the TxEddystone-UID app:
321      * {https://github.com/google/eddystone/blob/master/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/java/com/google/sample/txeddystone_uid/MainActivity.java}
322      */
txPowerFromFlag(int txPowerFlag)323     private static byte txPowerFromFlag(int txPowerFlag) {
324         switch (txPowerFlag) {
325             case AdvertiseSettings.ADVERTISE_TX_POWER_HIGH:
326                 return (byte) -16;
327             case AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM:
328                 return (byte) -26;
329             case AdvertiseSettings.ADVERTISE_TX_POWER_LOW:
330                 return (byte) -35;
331             case AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW:
332                 return (byte) -59;
333             default:
334                 throw new IllegalStateException("Unknown TxPower level=" + txPowerFlag);
335         }
336     }
337 
stopMultiAdvertising(int appIf)338     public void stopMultiAdvertising(int appIf) {
339         LOGGER.d(String.format("stopAdvertising(%d) on %s", appIf, mAddress));
340         Advertiser advertiser = mAdvertisers.get(appIf);
341         if (advertiser == null) {
342             LOGGER.d(String.format("Advertising already stopped on %s, clientIf: %d", mAddress,
343                     appIf));
344             return;
345         }
346         mAdvertisers.remove(appIf);
347         final IBluetoothGattCallback callback = mClientCallbacks.get(appIf);
348         @SuppressWarnings("unused") // go/futurereturn-lsc
349         Future<?> possiblyIgnoredError =
350                 DeviceShadowEnvironmentImpl.run(
351                         mAddress,
352                         () -> {
353                             callback.onMultiAdvertiseCallback(
354                                     BluetoothConstants.ADVERTISE_SUCCESS, false /* isStart */,
355                                     null /* setting */);
356                             return null;
357                         });
358     }
359 
startScan(final int appIf, ScanSettings settings, List<ScanFilter> filters)360     public void startScan(final int appIf, ScanSettings settings, List<ScanFilter> filters) {
361         LOGGER.d(String.format("startScan(%d) on %s", appIf, mAddress));
362         if (filters == null) {
363             filters = new ArrayList<>();
364         }
365         final Scanner scanner = new Scanner(appIf, settings, filters);
366         mScanners.put(appIf, scanner);
367         @SuppressWarnings("unused") // go/futurereturn-lsc
368         Future<?> possiblyIgnoredError =
369                 DeviceShadowEnvironmentImpl.run(
370                         mAddress,
371                         () -> {
372                             try {
373                                 scan(scanner);
374                             } catch (InterruptedException e) {
375                                 LOGGER.e(
376                                         String.format("Failed to scan on %s, clientIf: %d.",
377                                                 mAddress, scanner.mClientIf),
378                                         e);
379                             }
380                             return null;
381                         });
382     }
383 
384     // TODO(b/200231384): support periodic scan with interval and scan window.
scan(Scanner scanner)385     private void scan(Scanner scanner) throws InterruptedException {
386         // fetch existing advertisements
387         List<DeviceletImpl> devicelets = DeviceShadowEnvironmentImpl.getDeviceletImpls();
388         for (DeviceletImpl devicelet : devicelets) {
389             BlueletImpl bluelet = devicelet.blueletImpl();
390             if (bluelet.address.equals(mAddress)) {
391                 continue;
392             }
393             for (Advertiser advertiser : bluelet.getGattDelegate().mAdvertisers.values()) {
394                 if (VERSION.SDK_INT < 21) {
395                     throw new UnsupportedOperationException(
396                             String.format("API %d is not supported.", VERSION.SDK_INT));
397                 }
398 
399                 byte[] advertiseData =
400                         GattHelper.convertAdvertiseData(
401                                 advertiser.mAdvertiseData,
402                                 advertiser.mTxPowerLevel,
403                                 advertiser.mName,
404                                 advertiser.mSettings.isConnectable());
405                 byte[] scanResponse =
406                         GattHelper.convertAdvertiseData(
407                                 advertiser.mScanResponse,
408                                 advertiser.mTxPowerLevel,
409                                 advertiser.mName,
410                                 advertiser.mSettings.isConnectable());
411 
412                 ScanRecord scanRecord =
413                         ReflectionHelpers.callStaticMethod(
414                                 ScanRecord.class,
415                                 "parseFromBytes",
416                                 ClassParameter.from(byte[].class,
417                                         Bytes.concat(advertiseData, scanResponse)));
418                 ScanResult scanResult =
419                         new ScanResult(
420                                 BluetoothAdapter.getDefaultAdapter()
421                                         .getRemoteDevice(advertiser.mAddress),
422                                 scanRecord,
423                                 DEFAULT_RSSI,
424                                 SystemClock.elapsedRealtimeNanos());
425 
426                 if (!matchFilters(scanResult, scanner.mFilters)) {
427                     continue;
428                 }
429 
430                 IBluetoothGattCallback callback = mClientCallbacks.get(scanner.mClientIf);
431                 if (callback == null) {
432                     LOGGER.e(
433                             String.format("Callback is null on %s, clientIf: %d", mAddress,
434                                     scanner.mClientIf));
435                     return;
436                 }
437                 callback.onScanResult(scanResult);
438             }
439         }
440     }
441 
matchFilters(ScanResult scanResult, List<ScanFilter> filters)442     private boolean matchFilters(ScanResult scanResult, List<ScanFilter> filters) {
443         for (ScanFilter filter : filters) {
444             if (!filter.matches(scanResult)) {
445                 return false;
446             }
447         }
448         return true;
449     }
450 
stopScan(int appIf)451     public void stopScan(int appIf) {
452         LOGGER.d(String.format("stopScan(%d) on %s", appIf, mAddress));
453         Scanner scanner = mScanners.get(appIf);
454         if (scanner == null) {
455             LOGGER.d(
456                     String.format("Scanning already stopped on %s, clientIf: %d", mAddress, appIf));
457             return;
458         }
459         mScanners.remove(appIf);
460     }
461 
462     static class Service {
463 
464         private Map<ParcelUuid, Characteristic> mCharacteristics = new HashMap<>();
465         private ParcelUuid mUuid;
466 
Service(ParcelUuid uuid)467         Service(ParcelUuid uuid) {
468             this.mUuid = uuid;
469         }
470 
getCharacteristic(ParcelUuid uuid)471         Characteristic getCharacteristic(ParcelUuid uuid) {
472             return mCharacteristics.get(uuid);
473         }
474 
addCharacteristic(ParcelUuid uuid, int properties, int permissions)475         Characteristic addCharacteristic(ParcelUuid uuid, int properties, int permissions) {
476             Characteristic ch = new Characteristic(uuid, properties, permissions);
477             mCharacteristics.put(uuid, ch);
478             return ch;
479         }
480 
getCharacteristics()481         Collection<Characteristic> getCharacteristics() {
482             return mCharacteristics.values();
483         }
484 
getUuid()485         ParcelUuid getUuid() {
486             return this.mUuid;
487         }
488     }
489 
490     static class Characteristic {
491 
492         private int mProperties;
493         private ParcelUuid mUuid;
494         private Map<ParcelUuid, Descriptor> mDescriptors = new HashMap<>();
495         private Set<String> mNotifyClients = new HashSet<>();
496         private byte[] mValue;
497 
Characteristic(ParcelUuid uuid, int properties, int permissions)498         Characteristic(ParcelUuid uuid, int properties, int permissions) {
499             this.mProperties = properties;
500             this.mUuid = uuid;
501         }
502 
getDescriptor(ParcelUuid uuid)503         Descriptor getDescriptor(ParcelUuid uuid) {
504             return mDescriptors.get(uuid);
505         }
506 
addDescriptor(ParcelUuid uuid, int permissions)507         Descriptor addDescriptor(ParcelUuid uuid, int permissions) {
508             Descriptor desc = new Descriptor(uuid, permissions);
509             mDescriptors.put(uuid, desc);
510             return desc;
511         }
512 
getDescriptors()513         Collection<Descriptor> getDescriptors() {
514             return mDescriptors.values();
515         }
516 
setValue(byte[] value)517         void setValue(byte[] value) {
518             this.mValue = value;
519         }
520 
getValue()521         byte[] getValue() {
522             return mValue;
523         }
524 
getUuid()525         ParcelUuid getUuid() {
526             return mUuid;
527         }
528 
getProperties()529         int getProperties() {
530             return mProperties;
531         }
532 
registerNotification(String client, int clientIf)533         void registerNotification(String client, int clientIf) {
534             mNotifyClients.add(client);
535         }
536 
getNotifyClients()537         Set<String> getNotifyClients() {
538             return mNotifyClients;
539         }
540     }
541 
542     static class Descriptor {
543 
544         int mPermissions;
545         ParcelUuid mUuid;
546         byte[] mValue;
547 
Descriptor(ParcelUuid uuid, int permissions)548         Descriptor(ParcelUuid uuid, int permissions) {
549             this.mUuid = uuid;
550             this.mPermissions = permissions;
551         }
552 
setValue(byte[] value)553         void setValue(byte[] value) {
554             this.mValue = value;
555         }
556 
getValue()557         byte[] getValue() {
558             return mValue;
559         }
560 
getUuid()561         ParcelUuid getUuid() {
562             return mUuid;
563         }
564     }
565 
566     @VisibleForTesting
567     static class Advertiser {
568 
569         final int mClientIf;
570         final String mAddress;
571         final String mName;
572         final int mTxPowerLevel;
573         final AdvertiseData mAdvertiseData;
574         @Nullable
575         final AdvertiseData mScanResponse;
576         final AdvertiseSettings mSettings;
577 
Advertiser( int clientIf, String address, String name, int txPowerLevel, AdvertiseData advertiseData, AdvertiseData scanResponse, AdvertiseSettings settings)578         Advertiser(
579                 int clientIf,
580                 String address,
581                 String name,
582                 int txPowerLevel,
583                 AdvertiseData advertiseData,
584                 AdvertiseData scanResponse,
585                 AdvertiseSettings settings) {
586             this.mClientIf = clientIf;
587             this.mAddress = Preconditions.checkNotNull(address);
588             this.mName = name;
589             this.mTxPowerLevel = txPowerLevel;
590             this.mAdvertiseData = Preconditions.checkNotNull(advertiseData);
591             this.mScanResponse = scanResponse;
592             this.mSettings = Preconditions.checkNotNull(settings);
593         }
594     }
595 
596     @VisibleForTesting
597     static class Scanner {
598 
599         final int mClientIf;
600         final ScanSettings mSettings;
601         final List<ScanFilter> mFilters;
602 
Scanner(int clientIf, ScanSettings settings, List<ScanFilter> filters)603         Scanner(int clientIf, ScanSettings settings, List<ScanFilter> filters) {
604             this.mClientIf = clientIf;
605             this.mSettings = Preconditions.checkNotNull(settings);
606             this.mFilters = Preconditions.checkNotNull(filters);
607         }
608     }
609 }
610