1 /* 2 * Copyright (C) 2022 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 android.nearby.fastpair.provider; 18 19 import static com.google.common.io.BaseEncoding.base16; 20 21 import static java.nio.charset.StandardCharsets.UTF_8; 22 23 import android.bluetooth.BluetoothAdapter; 24 import android.bluetooth.BluetoothDevice; 25 import android.content.Context; 26 import android.content.SharedPreferences; 27 28 import androidx.annotation.Nullable; 29 30 import com.google.protobuf.ByteString; 31 32 import java.util.HashSet; 33 import java.util.Set; 34 import java.util.StringTokenizer; 35 36 /** Stores fast pair related information for each paired device */ 37 public class FastPairSimulatorDatabase { 38 39 private static final String SHARED_PREF_NAME = 40 "android.nearby.fastpair.provider.fastpairsimulator"; 41 private static final String KEY_DEVICE_NAME = "DEVICE_NAME"; 42 private static final String KEY_ACCOUNT_KEYS = "ACCOUNT_KEYS"; 43 private static final int MAX_NUMBER_OF_ACCOUNT_KEYS = 8; 44 45 // [for SASS] 46 private static final String KEY_FAST_PAIR_SEEKER_DEVICE = "FAST_PAIR_SEEKER_DEVICE"; 47 48 private final SharedPreferences mSharedPreferences; 49 FastPairSimulatorDatabase(Context context)50 public FastPairSimulatorDatabase(Context context) { 51 mSharedPreferences = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE); 52 } 53 54 /** Adds single account key. */ addAccountKey(byte[] accountKey)55 public void addAccountKey(byte[] accountKey) { 56 if (mSharedPreferences == null) { 57 return; 58 } 59 60 Set<ByteString> accountKeys = new HashSet<>(getAccountKeys()); 61 if (accountKeys.size() >= MAX_NUMBER_OF_ACCOUNT_KEYS) { 62 Set<ByteString> removedKeys = new HashSet<>(); 63 int removedCount = accountKeys.size() - MAX_NUMBER_OF_ACCOUNT_KEYS + 1; 64 for (ByteString key : accountKeys) { 65 if (removedKeys.size() == removedCount) { 66 break; 67 } 68 removedKeys.add(key); 69 } 70 71 accountKeys.removeAll(removedKeys); 72 } 73 74 // Just make sure the newest key will not be removed. 75 accountKeys.add(ByteString.copyFrom(accountKey)); 76 setAccountKeys(accountKeys); 77 } 78 79 /** Sets account keys, overrides all. */ setAccountKeys(Set<ByteString> accountKeys)80 public void setAccountKeys(Set<ByteString> accountKeys) { 81 if (mSharedPreferences == null) { 82 return; 83 } 84 85 Set<String> keys = new HashSet<>(); 86 for (ByteString item : accountKeys) { 87 keys.add(base16().encode(item.toByteArray())); 88 } 89 90 mSharedPreferences.edit().putStringSet(KEY_ACCOUNT_KEYS, keys).apply(); 91 } 92 93 /** Gets all account keys. */ getAccountKeys()94 public Set<ByteString> getAccountKeys() { 95 if (mSharedPreferences == null) { 96 return new HashSet<>(); 97 } 98 99 Set<String> keys = mSharedPreferences.getStringSet(KEY_ACCOUNT_KEYS, new HashSet<>()); 100 Set<ByteString> accountKeys = new HashSet<>(); 101 // Add new account keys one by one. 102 for (String key : keys) { 103 accountKeys.add(ByteString.copyFrom(base16().decode(key))); 104 } 105 106 return accountKeys; 107 } 108 109 /** Sets local device name. */ setLocalDeviceName(byte[] deviceName)110 public void setLocalDeviceName(byte[] deviceName) { 111 if (mSharedPreferences == null) { 112 return; 113 } 114 115 String humanReadableName = deviceName != null ? new String(deviceName, UTF_8) : null; 116 if (humanReadableName == null) { 117 mSharedPreferences.edit().remove(KEY_DEVICE_NAME).apply(); 118 } else { 119 mSharedPreferences.edit().putString(KEY_DEVICE_NAME, humanReadableName).apply(); 120 } 121 } 122 123 /** Gets local device name. */ 124 @Nullable getLocalDeviceName()125 public byte[] getLocalDeviceName() { 126 if (mSharedPreferences == null) { 127 return null; 128 } 129 130 String deviceName = mSharedPreferences.getString(KEY_DEVICE_NAME, null); 131 return deviceName != null ? deviceName.getBytes(UTF_8) : null; 132 } 133 134 /** 135 * [for SASS] Adds seeker device info. <a 136 * href="http://go/smart-audio-source-switching-design">Sass design doc</a> 137 */ addFastPairSeekerDevice(@ullable BluetoothDevice device, byte[] accountKey)138 public void addFastPairSeekerDevice(@Nullable BluetoothDevice device, byte[] accountKey) { 139 if (mSharedPreferences == null) { 140 return; 141 } 142 143 if (device == null) { 144 return; 145 } 146 147 // When hitting size limitation, choose the existing items to delete. 148 Set<FastPairSeekerDevice> fastPairSeekerDevices = getFastPairSeekerDevices(); 149 if (fastPairSeekerDevices.size() > MAX_NUMBER_OF_ACCOUNT_KEYS) { 150 int removedCount = fastPairSeekerDevices.size() - MAX_NUMBER_OF_ACCOUNT_KEYS + 1; 151 Set<FastPairSeekerDevice> removedFastPairDevices = new HashSet<>(); 152 for (FastPairSeekerDevice fastPairDevice : fastPairSeekerDevices) { 153 if (removedFastPairDevices.size() == removedCount) { 154 break; 155 } 156 removedFastPairDevices.add(fastPairDevice); 157 } 158 fastPairSeekerDevices.removeAll(removedFastPairDevices); 159 } 160 161 fastPairSeekerDevices.add(new FastPairSeekerDevice(device, accountKey)); 162 setFastPairSeekerDevices(fastPairSeekerDevices); 163 } 164 165 /** [for SASS] Sets all seeker device info, overrides all. */ setFastPairSeekerDevices(Set<FastPairSeekerDevice> fastPairSeekerDeviceSet)166 public void setFastPairSeekerDevices(Set<FastPairSeekerDevice> fastPairSeekerDeviceSet) { 167 if (mSharedPreferences == null) { 168 return; 169 } 170 171 Set<String> rawStringSet = new HashSet<>(); 172 for (FastPairSeekerDevice item : fastPairSeekerDeviceSet) { 173 rawStringSet.add(item.toRawString()); 174 } 175 176 mSharedPreferences.edit().putStringSet(KEY_FAST_PAIR_SEEKER_DEVICE, rawStringSet).apply(); 177 } 178 179 /** [for SASS] Gets all seeker device info. */ getFastPairSeekerDevices()180 public Set<FastPairSeekerDevice> getFastPairSeekerDevices() { 181 if (mSharedPreferences == null) { 182 return new HashSet<>(); 183 } 184 185 Set<FastPairSeekerDevice> fastPairSeekerDevices = new HashSet<>(); 186 Set<String> rawStringSet = 187 mSharedPreferences.getStringSet(KEY_FAST_PAIR_SEEKER_DEVICE, new HashSet<>()); 188 for (String rawString : rawStringSet) { 189 FastPairSeekerDevice fastPairDevice = FastPairSeekerDevice.fromRawString(rawString); 190 if (fastPairDevice == null) { 191 continue; 192 } 193 fastPairSeekerDevices.add(fastPairDevice); 194 } 195 196 return fastPairSeekerDevices; 197 } 198 199 /** Defines data structure for the paired Fast Pair device. */ 200 public static class FastPairSeekerDevice { 201 private static final int INDEX_DEVICE = 0; 202 private static final int INDEX_ACCOUNT_KEY = 1; 203 204 private final BluetoothDevice mDevice; 205 private final byte[] mAccountKey; 206 FastPairSeekerDevice(BluetoothDevice device, byte[] accountKey)207 private FastPairSeekerDevice(BluetoothDevice device, byte[] accountKey) { 208 this.mDevice = device; 209 this.mAccountKey = accountKey; 210 } 211 getBluetoothDevice()212 public BluetoothDevice getBluetoothDevice() { 213 return mDevice; 214 } 215 getAccountKey()216 public byte[] getAccountKey() { 217 return mAccountKey; 218 } 219 toRawString()220 public String toRawString() { 221 return String.format("%s,%s", mDevice, base16().encode(mAccountKey)); 222 } 223 224 /** Decodes the raw string if possible. */ 225 @Nullable fromRawString(String rawString)226 public static FastPairSeekerDevice fromRawString(String rawString) { 227 BluetoothDevice device = null; 228 byte[] accountKey = null; 229 int step = INDEX_DEVICE; 230 231 StringTokenizer tokenizer = new StringTokenizer(rawString, ","); 232 while (tokenizer.hasMoreElements()) { 233 boolean shouldStop = false; 234 String token = tokenizer.nextToken(); 235 switch (step) { 236 case INDEX_DEVICE: 237 try { 238 device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(token); 239 } catch (IllegalArgumentException e) { 240 device = null; 241 } 242 break; 243 case INDEX_ACCOUNT_KEY: 244 accountKey = base16().decode(token); 245 if (accountKey.length != 16) { 246 accountKey = null; 247 } 248 break; 249 default: 250 shouldStop = true; 251 } 252 253 if (shouldStop) { 254 break; 255 } 256 step++; 257 } 258 if (device != null && accountKey != null) { 259 return new FastPairSeekerDevice(device, accountKey); 260 } 261 return null; 262 } 263 } 264 } 265