• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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