1 /* 2 * Copyright (C) 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.google.android.car.kitchensink.bluetooth; 18 19 import android.Manifest; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothManager; 22 import android.bluetooth.BluetoothServerSocket; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.os.Bundle; 28 import android.util.Log; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.Button; 33 import android.widget.CompoundButton; 34 import android.widget.EditText; 35 import android.widget.Switch; 36 import android.widget.Toast; 37 38 import androidx.annotation.Nullable; 39 import androidx.fragment.app.Fragment; 40 41 import com.google.android.car.kitchensink.KitchenSinkActivity; 42 import com.google.android.car.kitchensink.R; 43 44 import java.math.BigInteger; 45 import java.time.Duration; 46 import java.util.Arrays; 47 import java.util.Iterator; 48 import java.util.List; 49 import java.util.Objects; 50 import java.util.UUID; 51 import java.util.concurrent.TimeUnit; 52 import java.util.concurrent.locks.Condition; 53 import java.util.concurrent.locks.ReentrantLock; 54 55 public class CustomUuidEirFragment extends Fragment { 56 private static final String TAG = "CAR.BLUETOOTH.KS"; 57 58 private static final int DISCOVERABLE_TIMEOUT_TWO_MINUTES = 120_000; 59 private static final int ADAPTER_ON_TIMEOUT_MS = 1_000; 60 61 BluetoothAdapter mAdapter; 62 int mScanModeNotDiscoverable; 63 64 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 65 @Override 66 public void onReceive(Context context, Intent intent) { 67 String action = intent.getAction(); 68 if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { 69 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); 70 if ((state == BluetoothAdapter.STATE_ON) 71 || (state == BluetoothAdapter.STATE_OFF)) { 72 refreshUi(); 73 if (state == BluetoothAdapter.STATE_ON) { 74 mAdapterOnLock.lock(); 75 mAdapterOnCondition.signal(); 76 mAdapterOnLock.unlock(); 77 } 78 } 79 } else if (BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(action)) { 80 refreshUi(); 81 } 82 } 83 }; 84 85 // Some functions require the Bluetooth adapter to be on, and will enable if it isn't already. 86 // However, there is some latency for the adapter to reach the ON state. 87 private final ReentrantLock mAdapterOnLock = new ReentrantLock(); 88 private final Condition mAdapterOnCondition = mAdapterOnLock.newCondition(); 89 90 Switch mAdvertisingToggle; 91 Switch mBtAdapterToggle; 92 Button mResetUuidDefaults; 93 List<EditText> mUuidEditTexts; 94 List<Switch> mUuidSwitches; 95 96 private static final String UUID_SERVICE_NAME = "Custom UUID test"; 97 private static final String UUID1_DEFAULT = "01234567-89ab-cdef-0123-456789abcdef"; 98 private static final String UUID2_DEFAULT = "fedcba98-7654-3210-fedc-ba9876543210"; 99 private static final String UUID3_DEFAULT = new StringBuilder(String.format("%032x", 100 new BigInteger(1, "Hello World! Hi!".getBytes()))).insert(20, "-").insert(16, "-") 101 .insert(12, "-").insert(8, "-").toString(); 102 private static final String UUID4_DEFAULT = new StringBuilder(String.format("%032x", 103 new BigInteger(1, "Foo!Bar!Baz!Fum!".getBytes()))).insert(20, "-").insert(16, "-") 104 .insert(12, "-").insert(8, "-").toString(); 105 private static final List<String> DEFAULT_UUIDS = 106 Arrays.asList(UUID1_DEFAULT, UUID2_DEFAULT, UUID3_DEFAULT, UUID4_DEFAULT); 107 108 @Override onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)109 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 110 @Nullable Bundle savedInstanceState) { 111 View v = inflater.inflate(R.layout.bluetooth_uuid_eir, container, false); 112 113 mAdvertisingToggle = (Switch) v.findViewById(R.id.advertising_toggle); 114 mBtAdapterToggle = (Switch) v.findViewById(R.id.bt_adapter_toggle); 115 116 if (!BluetoothPermissionChecker.isPermissionGranted( 117 (KitchenSinkActivity) getHost(), Manifest.permission.BLUETOOTH_CONNECT) 118 || !BluetoothPermissionChecker.isPermissionGranted( 119 (KitchenSinkActivity) getHost(), Manifest.permission.BLUETOOTH_SCAN)) { 120 BluetoothPermissionChecker.requestMultiplePermissions( 121 new String[]{Manifest.permission.BLUETOOTH_CONNECT, 122 Manifest.permission.BLUETOOTH_SCAN}, 123 this, 124 () -> { 125 mAdvertisingToggle.setEnabled(true); 126 mBtAdapterToggle.setEnabled(true); 127 }, 128 () -> { 129 mAdvertisingToggle.setEnabled(false); 130 mBtAdapterToggle.setEnabled(false); 131 Toast.makeText(getContext(), 132 "UUID test cannot run without Bluetooth permissions. " 133 + "(You can change permissions in Settings.)", 134 Toast.LENGTH_SHORT).show(); 135 } 136 ); 137 } 138 139 mResetUuidDefaults = (Button) v.findViewById(R.id.reset_uuid_defaults); 140 141 mUuidEditTexts = Arrays.asList( 142 (EditText) v.findViewById(R.id.uuid1), 143 (EditText) v.findViewById(R.id.uuid2), 144 (EditText) v.findViewById(R.id.uuid3), 145 (EditText) v.findViewById(R.id.uuid4)); 146 mUuidSwitches = Arrays.asList( 147 (Switch) v.findViewById(R.id.uuid1_toggle), 148 (Switch) v.findViewById(R.id.uuid2_toggle), 149 (Switch) v.findViewById(R.id.uuid3_toggle), 150 (Switch) v.findViewById(R.id.uuid4_toggle)); 151 152 mAdvertisingToggle.setOnCheckedChangeListener( 153 (buttonView, isChecked) -> setAdvertisingState(isChecked)); 154 155 mBtAdapterToggle.setOnCheckedChangeListener( 156 (buttonView, isChecked) -> setAdapterState(isChecked)); 157 158 mResetUuidDefaults.setOnClickListener(new View.OnClickListener() { 159 @Override 160 public void onClick(View view) { 161 setUuidsToDefault(); 162 } 163 }); 164 165 // Associating each {@link EditText} used for entering UUIDs with its corresponding 166 // {@link Switch}. 167 Iterator<EditText> textIter = mUuidEditTexts.iterator(); 168 Iterator<Switch> switchIter = mUuidSwitches.iterator(); 169 Switch toggle; 170 while (textIter.hasNext() && switchIter.hasNext()) { 171 toggle = switchIter.next(); 172 toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 173 EditText mUuidText = textIter.next(); 174 BluetoothServerSocket mSocket; 175 /** 176 * When the API {@link BluetoothAdapter#listenUsingRfcommWithServiceRecord} is 177 * called, the 128-bit UUID is added to the EIR. When the socket created by that 178 * API is closed, the UUID is deleted from the EIR. 179 */ 180 @Override 181 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 182 mSocket = setUuidInEir(buttonView, isChecked, mUuidText, mSocket); 183 } 184 }); 185 } 186 // Disable each UUID's {@link Switch} if Adapter or Advertising toggles are not enabled, 187 // which indicates Bluetooth permissions have not been granted. 188 if (!mAdvertisingToggle.isEnabled() || !mBtAdapterToggle.isEnabled()) { 189 while (switchIter.hasNext()) { 190 toggle = switchIter.next(); 191 toggle.setEnabled(false); 192 } 193 } 194 195 setUuidsToDefault(); 196 197 BluetoothManager bluetoothManager = 198 Objects.requireNonNull(getContext().getSystemService(BluetoothManager.class)); 199 mAdapter = Objects.requireNonNull(bluetoothManager.getAdapter()); 200 201 // We don't know if "OFF" is {@link BluetoothAdapter#SCAN_MODE_NONE} 202 // or {@link BluetoothAdapter#SCAN_MODE_CONNECTABLE}. If the current scan mode is 203 // {@link BluetoothAdapter#SCAN_MODE_CONNECTABLE_DISCOVERABLE}, then we'll set "OFF" to 204 // {@link BluetoothAdapter#SCAN_MODE_CONNECTABLE}. 205 if (BluetoothPermissionChecker.isPermissionGranted( 206 (KitchenSinkActivity) getHost(), Manifest.permission.BLUETOOTH_SCAN)) { 207 mScanModeNotDiscoverable = mAdapter.getScanMode(); 208 Log.d(TAG, "Original scan mode was: " + mScanModeNotDiscoverable + ", " 209 + scanModeToText(mScanModeNotDiscoverable)); 210 } 211 if (mScanModeNotDiscoverable == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { 212 mScanModeNotDiscoverable = BluetoothAdapter.SCAN_MODE_CONNECTABLE; 213 } 214 215 IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); 216 filter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); 217 getContext().registerReceiver(mReceiver, filter); 218 219 return v; 220 } 221 222 @Override onStart()223 public void onStart() { 224 super.onStart(); 225 } 226 227 @Override onStop()228 public void onStop() { 229 // Turn off advertising toggle to stop advertising. 230 mAdvertisingToggle.setChecked(false); 231 // Turn off UUID switches to remove them from EIR. 232 turnOffUuidSwitches(); 233 234 super.onStop(); 235 getContext().unregisterReceiver(mReceiver); 236 } 237 238 @Override onResume()239 public void onResume() { 240 if (BluetoothPermissionChecker.isPermissionGranted( 241 (KitchenSinkActivity) getHost(), Manifest.permission.BLUETOOTH_CONNECT)) { 242 mBtAdapterToggle.setEnabled(true); 243 } else { 244 mBtAdapterToggle.setEnabled(false); 245 } 246 if (BluetoothPermissionChecker.isPermissionGranted( 247 (KitchenSinkActivity) getHost(), Manifest.permission.BLUETOOTH_SCAN)) { 248 mAdvertisingToggle.setEnabled(true); 249 } else { 250 mAdvertisingToggle.setEnabled(false); 251 } 252 super.onResume(); 253 refreshUi(); 254 } 255 256 /** 257 * When the API {@link BluetoothAdapter#listenUsingRfcommWithServiceRecord} is called, the 258 * 128-bit UUID is added to the EIR. When the socket created by that API is closed, the 259 * UUID is deleted from the EIR. 260 */ setUuidInEir(CompoundButton buttonView, boolean isChecked, EditText uuidText, BluetoothServerSocket socket)261 private BluetoothServerSocket setUuidInEir(CompoundButton buttonView, boolean isChecked, 262 EditText uuidText, BluetoothServerSocket socket) { 263 if (isChecked) { 264 // Add the corresponding UUID to the EIR 265 if (uuidText == null) { 266 Log.e(TAG, "setUuidInEir: Can't find EditText corresponding to toggle"); 267 return null; 268 } 269 uuidText.setEnabled(false); 270 UUID uuid; 271 try { 272 uuid = UUID.fromString(uuidText.getText().toString()); 273 } catch (IllegalArgumentException e) { 274 Log.e(TAG, "setUuidInEir: Invalid UUID format (hyphens matter!)"); 275 Toast.makeText(getContext(), 276 "Invalid UUID format (hyphens matter!)", 277 Toast.LENGTH_SHORT).show(); 278 buttonView.setChecked(false); 279 uuidText.setEnabled(true); 280 return null; 281 } 282 // Can't create a socket if adapter is not enabled. 283 if (!mAdapter.isEnabled()) { 284 mAdapter.enable(); 285 if (!waitForAdapterOn(ADAPTER_ON_TIMEOUT_MS)) { 286 return null; 287 } 288 } 289 try { 290 socket = mAdapter.listenUsingRfcommWithServiceRecord( 291 UUID_SERVICE_NAME, uuid); 292 } catch (Exception e) { 293 Log.e(TAG, "setUuidInEir: Can't create socket"); 294 Toast.makeText(getContext(), 295 "Can't create socket. Verify adapter is ON?", 296 Toast.LENGTH_SHORT).show(); 297 buttonView.setChecked(false); 298 uuidText.setEnabled(true); 299 return null; 300 } 301 return socket; 302 } else { 303 // Remove the existing UUID from the EIR 304 if (uuidText == null) { 305 Log.e(TAG, "setUuidInEir: Can't find EditText corresponding to toggle"); 306 return null; 307 } 308 uuidText.setEnabled(true); 309 if (socket == null) { 310 Log.w(TAG, "setUuidInEir: Can't find socket corresponding to toggle"); 311 return null; 312 } 313 try { 314 socket.close(); 315 } catch (Exception e) { 316 Log.e(TAG, "setUuidInEir: Can't close socket"); 317 } 318 return null; 319 } 320 } 321 setAdvertisingState(boolean isChecked)322 private void setAdvertisingState(boolean isChecked) { 323 if (isChecked) { 324 // Start advertising EIR by entering discoverable mode. 325 if (!mAdapter.isEnabled()) { 326 mAdapter.enable(); 327 if (!waitForAdapterOn(ADAPTER_ON_TIMEOUT_MS)) { 328 return; 329 } 330 } 331 mAdapter.setDiscoverableTimeout(Duration.ofMillis(DISCOVERABLE_TIMEOUT_TWO_MINUTES)); 332 mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); 333 Log.d(TAG, "Started discovery."); 334 } else { 335 // Stop advertising. 336 Log.d(TAG, "Stopping discovery, setting scan mode to: " + mScanModeNotDiscoverable 337 + ", " + scanModeToText(mScanModeNotDiscoverable)); 338 mAdapter.setScanMode(mScanModeNotDiscoverable); 339 } 340 } 341 setAdapterState(boolean isChecked)342 private void setAdapterState(boolean isChecked) { 343 if (isChecked) { 344 mAdapter.enable(); 345 } else { 346 mAdapter.disable(); 347 } 348 } 349 refreshUi()350 private void refreshUi() { 351 if (mBtAdapterToggle.isEnabled()) { 352 mBtAdapterToggle.setChecked(mAdapter.isEnabled()); 353 } 354 if (mAdvertisingToggle.isEnabled()) { 355 mAdvertisingToggle.setChecked(mAdapter.getScanMode() 356 == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); 357 } 358 } 359 turnOffUuidSwitches()360 private void turnOffUuidSwitches() { 361 for (Iterator<Switch> i = mUuidSwitches.iterator(); i.hasNext(); ) { 362 i.next().setChecked(false); 363 } 364 } 365 setUuidsToDefault()366 private void setUuidsToDefault() { 367 turnOffUuidSwitches(); 368 Iterator<EditText> textIter = mUuidEditTexts.iterator(); 369 Iterator<String> uuidIter = DEFAULT_UUIDS.iterator(); 370 while (textIter.hasNext() && uuidIter.hasNext()) { 371 textIter.next().setText(uuidIter.next()); 372 } 373 } 374 scanModeToText(int mode)375 private String scanModeToText(int mode) { 376 switch (mode) { 377 case BluetoothAdapter.SCAN_MODE_NONE: 378 return "None"; 379 case BluetoothAdapter.SCAN_MODE_CONNECTABLE: 380 return "Connectable"; 381 case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE: 382 return "Connectable+Discoverable"; 383 default: 384 return "Unknown"; 385 } 386 } 387 waitForAdapterOn(int timeout)388 private boolean waitForAdapterOn(int timeout) { 389 mAdapterOnLock.lock(); 390 try { 391 while (!mAdapter.isEnabled()) { 392 Toast.makeText(getContext(), "Waiting for adapter to turn ON", 393 Toast.LENGTH_SHORT).show(); 394 if (mAdapterOnCondition.await(timeout, TimeUnit.MILLISECONDS)) { 395 Log.w(TAG, "waitForAdapterOn: timed out"); 396 Toast.makeText(getContext(), "Timed out waiting for adapter to turn ON", 397 Toast.LENGTH_SHORT).show(); 398 return false; 399 } 400 } 401 } catch (InterruptedException e) { 402 Log.e(TAG, "waitForAdapterOn: " + e); 403 Toast.makeText(getContext(), "Exception when waiting for adapter to turn ON", 404 Toast.LENGTH_SHORT).show(); 405 return false; 406 } finally { 407 mAdapterOnLock.unlock(); 408 } 409 return true; 410 } 411 } 412