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 
18 package android.companion;
19 
20 import static android.text.TextUtils.firstNotEmpty;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.bluetooth.BluetoothDevice;
25 import android.compat.annotation.UnsupportedAppUsage;
26 import android.net.wifi.ScanResult;
27 import android.os.Build;
28 import android.os.ParcelUuid;
29 import android.os.Parcelable;
30 import android.util.Log;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 
34 import java.util.Arrays;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.Objects;
38 import java.util.UUID;
39 import java.util.regex.Pattern;
40 
41 /** @hide */
42 public class BluetoothDeviceFilterUtils {
BluetoothDeviceFilterUtils()43     private BluetoothDeviceFilterUtils() {}
44 
45     private static final boolean DEBUG = false;
46     private static final String LOG_TAG = "CDM_BluetoothDeviceFilterUtils";
47 
48     @Nullable
patternToString(@ullable Pattern p)49     static String patternToString(@Nullable Pattern p) {
50         return p == null ? null : p.pattern();
51     }
52 
53     @Nullable
patternFromString(@ullable String s)54     static Pattern patternFromString(@Nullable String s) {
55         return s == null ? null : Pattern.compile(s);
56     }
57 
matchesAddress(String deviceAddress, BluetoothDevice device)58     static boolean matchesAddress(String deviceAddress, BluetoothDevice device) {
59         final boolean result = deviceAddress == null
60                 || (device != null && deviceAddress.equals(device.getAddress()));
61         if (DEBUG) debugLogMatchResult(result, device, deviceAddress);
62         return result;
63     }
64 
matchesServiceUuids(List<ParcelUuid> serviceUuids, List<ParcelUuid> serviceUuidMasks, BluetoothDevice device)65     static boolean matchesServiceUuids(List<ParcelUuid> serviceUuids,
66             List<ParcelUuid> serviceUuidMasks, BluetoothDevice device) {
67         for (int i = 0; i < serviceUuids.size(); i++) {
68             ParcelUuid uuid = serviceUuids.get(i);
69             ParcelUuid uuidMask = serviceUuidMasks.get(i);
70             if (!matchesServiceUuid(uuid, uuidMask, device)) {
71                 return false;
72             }
73         }
74         return true;
75     }
76 
matchesServiceUuid(ParcelUuid serviceUuid, ParcelUuid serviceUuidMask, BluetoothDevice device)77     static boolean matchesServiceUuid(ParcelUuid serviceUuid, ParcelUuid serviceUuidMask,
78             BluetoothDevice device) {
79         boolean result = false;
80         List<ParcelUuid> deviceUuids = device.getUuids() == null
81                 ? Collections.emptyList() : Arrays.asList(device.getUuids());
82         if (serviceUuid == null) {
83             result = true;
84         } else {
85             for (ParcelUuid parcelUuid : deviceUuids) {
86                 UUID uuidMask = serviceUuidMask == null ? null : serviceUuidMask.getUuid();
87                 if (uuidsMaskedEquals(parcelUuid.getUuid(), serviceUuid.getUuid(), uuidMask)) {
88                     result = true;
89                 }
90             }
91         }
92         if (DEBUG) debugLogMatchResult(result, device, serviceUuid);
93         return result;
94     }
95 
matchesName(@ullable Pattern namePattern, BluetoothDevice device)96     static boolean matchesName(@Nullable Pattern namePattern, BluetoothDevice device) {
97         boolean result;
98         if (namePattern == null)  {
99             result = true;
100         } else if (device == null) {
101             result = false;
102         } else {
103             final String name = device.getName();
104             result = name != null && namePattern.matcher(name).find();
105         }
106         if (DEBUG) debugLogMatchResult(result, device, namePattern);
107         return result;
108     }
109 
matchesName(@ullable Pattern namePattern, ScanResult device)110     static boolean matchesName(@Nullable Pattern namePattern, ScanResult device) {
111         boolean result;
112         if (namePattern == null)  {
113             result = true;
114         } else if (device == null) {
115             result = false;
116         } else {
117             final String name = device.SSID;
118             result = name != null && namePattern.matcher(name).find();
119         }
120         if (DEBUG) debugLogMatchResult(result, device, namePattern);
121         return result;
122     }
123 
debugLogMatchResult( boolean result, BluetoothDevice device, Object criteria)124     private static void debugLogMatchResult(
125             boolean result, BluetoothDevice device, Object criteria) {
126         Log.i(LOG_TAG, getDeviceDisplayNameInternal(device) + (result ? " ~ " : " !~ ") + criteria);
127     }
128 
debugLogMatchResult( boolean result, ScanResult device, Object criteria)129     private static void debugLogMatchResult(
130             boolean result, ScanResult device, Object criteria) {
131         Log.i(LOG_TAG, getDeviceDisplayNameInternal(device) + (result ? " ~ " : " !~ ") + criteria);
132     }
133 
134     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getDeviceDisplayNameInternal(@onNull BluetoothDevice device)135     public static String getDeviceDisplayNameInternal(@NonNull BluetoothDevice device) {
136         return firstNotEmpty(device.getAlias(), device.getAddress());
137     }
138 
139     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getDeviceDisplayNameInternal(@onNull ScanResult device)140     public static String getDeviceDisplayNameInternal(@NonNull ScanResult device) {
141         return firstNotEmpty(device.SSID, device.BSSID);
142     }
143 
144     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getDeviceMacAddress(@onNull Parcelable device)145     public static String getDeviceMacAddress(@NonNull Parcelable device) {
146         if (device instanceof BluetoothDevice) {
147             return ((BluetoothDevice) device).getAddress();
148         } else if (device instanceof ScanResult) {
149             return ((ScanResult) device).BSSID;
150         } else if (device instanceof android.bluetooth.le.ScanResult) {
151             return getDeviceMacAddress(((android.bluetooth.le.ScanResult) device).getDevice());
152         } else {
153             throw new IllegalArgumentException("Unknown device type: " + device);
154         }
155     }
156 
157     /**
158      * Compares two {@link #UUID} with a {@link #UUID} mask.
159      *
160      * @param data first {@link #UUID}.
161      * @param uuid second {@link #UUID}.
162      * @param mask mask {@link #UUID}.
163      * @return true if both UUIDs are equals when masked, false otherwise.
164      */
165     @VisibleForTesting
uuidsMaskedEquals(UUID data, UUID uuid, UUID mask)166     public static boolean uuidsMaskedEquals(UUID data, UUID uuid, UUID mask) {
167         if (mask == null) {
168             return Objects.equals(data, uuid);
169         }
170         return (data.getLeastSignificantBits() & mask.getLeastSignificantBits())
171                 == (uuid.getLeastSignificantBits() & mask.getLeastSignificantBits())
172                 && (data.getMostSignificantBits() & mask.getMostSignificantBits())
173                 == (uuid.getMostSignificantBits() & mask.getMostSignificantBits());
174     }
175 }
176