1 /*
2  * Copyright (C) 2016 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.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothDevicePicker;
22 import android.bluetooth.BluetoothHeadsetClient;
23 import android.bluetooth.BluetoothHeadsetClientCall;
24 import android.bluetooth.BluetoothProfile;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.os.Bundle;
30 import android.util.Log;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.widget.Button;
35 import android.widget.EditText;
36 import android.widget.TextView;
37 import android.widget.Toast;
38 
39 import androidx.annotation.Nullable;
40 import androidx.fragment.app.Fragment;
41 
42 import com.google.android.car.kitchensink.KitchenSinkActivity;
43 import com.google.android.car.kitchensink.R;
44 
45 public class BluetoothHeadsetFragment extends Fragment {
46     private static final String TAG = "CAR.BLUETOOTH.KS";
47     BluetoothAdapter mBluetoothAdapter;
48     BluetoothDevice mPickedDevice;
49 
50     TextView mPickedDeviceText;
51     Button mDevicePicker;
52     Button mConnect;
53     Button mDisconnect;
54     Button mScoConnect;
55     Button mScoDisconnect;
56     Button mEnableQuietMode;
57     Button mHoldCall;
58     Button mEnableBVRA;
59     Button mDisableBVRA;
60     Button mStartOutgoingCall;
61     Button mEndOutgoingCall;
62     EditText mOutgoingPhoneNumber;
63 
64     BluetoothHeadsetClient mHfpClientProfile;
65     BluetoothHeadsetClientCall mOutgoingCall;
66 
67     // Intent for picking a Bluetooth device
68     public static final String DEVICE_PICKER_ACTION =
69         "android.bluetooth.devicepicker.action.LAUNCH";
70 
71     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)72     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
73         @Nullable Bundle savedInstanceState) {
74         View v = inflater.inflate(R.layout.bluetooth_headset, container, false);
75 
76         mPickedDeviceText = (TextView) v.findViewById(R.id.bluetooth_device);
77         mDevicePicker = (Button) v.findViewById(R.id.bluetooth_pick_device);
78         mConnect = (Button) v.findViewById(R.id.bluetooth_headset_connect);
79         mDisconnect = (Button) v.findViewById(R.id.bluetooth_headset_disconnect);
80         mScoConnect = (Button) v.findViewById(R.id.bluetooth_sco_connect);
81         mScoDisconnect = (Button) v.findViewById(R.id.bluetooth_sco_disconnect);
82         mEnableQuietMode = (Button) v.findViewById(R.id.bluetooth_quiet_mode_enable);
83         mHoldCall = (Button) v.findViewById(R.id.bluetooth_hold_call);
84         mEnableBVRA = (Button) v.findViewById(R.id.bluetooth_voice_recognition_enable);
85         mDisableBVRA = (Button) v.findViewById(R.id.bluetooth_voice_recognition_disable);
86         mStartOutgoingCall = (Button) v.findViewById(R.id.bluetooth_start_outgoing_call);
87         mEndOutgoingCall = (Button) v.findViewById(R.id.bluetooth_end_outgoing_call);
88         mOutgoingPhoneNumber = (EditText) v.findViewById(R.id.bluetooth_outgoing_phone_number);
89 
90         if (!BluetoothConnectionPermissionChecker.isPermissionGranted(
91                 (KitchenSinkActivity) getHost())) {
92             BluetoothConnectionPermissionChecker.requestPermission(this,
93                     this::setDevicePickerButtonClickable,
94                     () -> {
95                         setDevicePickerButtonUnclickable();
96                         Toast.makeText(getContext(),
97                                 "Device picker can't run without BLUETOOTH_CONNECT permission. "
98                                         + "(You can change permissions in Settings.)",
99                                 Toast.LENGTH_SHORT).show();
100                     }
101             );
102         }
103 
104         // Connect profile
105         mConnect.setOnClickListener(new View.OnClickListener() {
106             @Override
107             public void onClick(View view) {
108                 connect();
109             }
110         });
111 
112         // Disonnect profile
113         mDisconnect.setOnClickListener(new View.OnClickListener() {
114             @Override
115             public void onClick(View view) {
116                 disconnect();
117             }
118         });
119 
120         // Connect SCO
121         mScoConnect.setOnClickListener(new View.OnClickListener() {
122             @Override
123             public void onClick(View view) {
124                 connectSco();
125             }
126         });
127 
128         // Disconnect SCO
129         mScoDisconnect.setOnClickListener(new View.OnClickListener() {
130             @Override
131             public void onClick(View view) {
132                 disconnectSco();
133             }
134         });
135 
136         // Enable quiet mode
137         mEnableQuietMode.setOnClickListener(new View.OnClickListener() {
138             @Override
139             public void onClick(View view) {
140                 mBluetoothAdapter.enableNoAutoConnect();
141             }
142         });
143 
144         // Place the current call on hold
145         mHoldCall.setOnClickListener(new View.OnClickListener() {
146             @Override
147             public void onClick(View view) {
148                 holdCall();
149             }
150         });
151 
152         // Enable Voice Recognition
153         mEnableBVRA.setOnClickListener(new View.OnClickListener() {
154             @Override
155             public void onClick(View view) {
156                 startBVRA();
157             }
158         });
159 
160         // Disable Voice Recognition
161         mDisableBVRA.setOnClickListener(new View.OnClickListener() {
162             @Override
163             public void onClick(View view) {
164                 stopBVRA();
165             }
166         });
167 
168         // Start a outgoing call
169         mStartOutgoingCall.setOnClickListener(new View.OnClickListener() {
170             @Override
171             public void onClick(View view) {
172                 startCall();
173             }
174         });
175 
176         // Stop a outgoing call
177         mEndOutgoingCall.setOnClickListener(new View.OnClickListener() {
178             @Override
179             public void onClick(View view) {
180                 stopCall();
181             }
182         });
183 
184         return v;
185     }
186 
setDevicePickerButtonClickable()187     private void setDevicePickerButtonClickable() {
188         mDevicePicker.setClickable(true);
189 
190         // Pick a bluetooth device
191         mDevicePicker.setOnClickListener(new View.OnClickListener() {
192             @Override
193             public void onClick(View view) {
194                 launchDevicePicker();
195             }
196         });
197     }
198 
setDevicePickerButtonUnclickable()199     private void setDevicePickerButtonUnclickable() {
200         mDevicePicker.setClickable(false);
201     }
202 
launchDevicePicker()203     void launchDevicePicker() {
204         IntentFilter filter = new IntentFilter();
205         filter.addAction(BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
206         getContext().registerReceiver(mPickerReceiver, filter);
207 
208         Intent intent = new Intent(DEVICE_PICKER_ACTION);
209         intent.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
210         getContext().startActivity(intent);
211     }
212 
connect()213     void connect() {
214         if (mPickedDevice == null) {
215             Log.w(TAG, "Device null when trying to connect sco!");
216             return;
217         }
218 
219         // Check if we have the proxy and connect the device.
220         if (mHfpClientProfile == null) {
221             Log.w(TAG, "HFP Profile proxy not available, cannot connect sco to " + mPickedDevice);
222             return;
223         }
224         mHfpClientProfile.connect(mPickedDevice);
225     }
226 
disconnect()227     void disconnect() {
228         if (mPickedDevice == null) {
229             Log.w(TAG, "Device null when trying to connect sco!");
230             return;
231         }
232 
233         // Check if we have the proxy and connect the device.
234         if (mHfpClientProfile == null) {
235             Log.w(TAG, "HFP Profile proxy not available, cannot connect sco to " + mPickedDevice);
236             return;
237         }
238         mHfpClientProfile.disconnect(mPickedDevice);
239     }
240 
connectSco()241     void connectSco() {
242         if (mPickedDevice == null) {
243             Log.w(TAG, "Device null when trying to connect sco!");
244             return;
245         }
246 
247         // Check if we have the proxy and connect the device.
248         if (mHfpClientProfile == null) {
249             Log.w(TAG, "HFP Profile proxy not available, cannot connect sco to " + mPickedDevice);
250             return;
251         }
252         mHfpClientProfile.connectAudio(mPickedDevice);
253     }
254 
disconnectSco()255     void disconnectSco() {
256         if (mPickedDevice == null) {
257             Log.w(TAG, "Device null when trying to disconnect sco!");
258             return;
259         }
260 
261         if (mHfpClientProfile == null) {
262             Log.w(TAG, "HFP Profile proxy not available, cannot disconnect sco to " +
263                 mPickedDevice);
264             return;
265         }
266         mHfpClientProfile.disconnectAudio(mPickedDevice);
267     }
268 
holdCall()269     void holdCall() {
270         if (mPickedDevice == null) {
271             Log.w(TAG, "Device null when trying to put the call on hold!");
272             return;
273         }
274 
275         if (mHfpClientProfile == null) {
276             Log.w(TAG, "HFP Profile proxy not available, cannot put the call on hold " +
277                 mPickedDevice);
278             return;
279         }
280         mHfpClientProfile.holdCall(mPickedDevice);
281     }
282 
startBVRA()283     void startBVRA() {
284         if (mPickedDevice == null) {
285             Log.w(TAG, "Device null when trying to start voice recognition!");
286             return;
287         }
288 
289         // Check if we have the proxy and connect the device.
290         if (mHfpClientProfile == null) {
291             Log.w(TAG, "HFP Profile proxy not available, cannot start voice recognition to "
292                     + mPickedDevice);
293             return;
294         }
295         mHfpClientProfile.startVoiceRecognition(mPickedDevice);
296     }
297 
stopBVRA()298     void stopBVRA() {
299         if (mPickedDevice == null) {
300             Log.w(TAG, "Device null when trying to stop voice recognition!");
301             return;
302         }
303 
304         // Check if we have the proxy and connect the device.
305         if (mHfpClientProfile == null) {
306             Log.w(TAG, "HFP Profile proxy not available, cannot stop voice recognition to "
307                     + mPickedDevice);
308             return;
309         }
310         mHfpClientProfile.stopVoiceRecognition(mPickedDevice);
311     }
312 
startCall()313     void startCall() {
314         if (mPickedDevice == null) {
315             Log.w(TAG, "Device null when trying to start voice call!");
316             return;
317         }
318 
319         // Check if we have the proxy and connect the device.
320         if (mHfpClientProfile == null) {
321             Log.w(TAG, "HFP Profile proxy not available, cannot start voice call to "
322                     + mPickedDevice);
323             return;
324         }
325 
326         if (mOutgoingCall != null) {
327             Log.w(TAG, "Potential on-going call or a stale call " + mOutgoingCall);
328         }
329 
330         String number = mOutgoingPhoneNumber.getText().toString();
331         mOutgoingCall = mHfpClientProfile.dial(mPickedDevice, number);
332         if (mOutgoingCall == null) {
333             Log.w(TAG, "Fail to dial number " + number + ". Make sure profile connect first.");
334         } else {
335             Log.d(TAG, "Succeed in creating outgoing call " + mOutgoingCall + " for number "
336                     + number);
337         }
338     }
339 
stopCall()340     void stopCall() {
341         if (mPickedDevice == null) {
342             Log.w(TAG, "Device null when trying to stop voice call!");
343             return;
344         }
345 
346         // Check if we have the proxy and connect the device.
347         if (mHfpClientProfile == null) {
348             Log.w(TAG, "HFP Profile proxy not available, cannot stop voice call to "
349                     + mPickedDevice);
350             return;
351         }
352 
353         if (mOutgoingCall != null) {
354             if (mHfpClientProfile.terminateCall(mPickedDevice, mOutgoingCall)) {
355                 Log.d(TAG, "Succeed in terminating outgoing call " + mOutgoingCall);
356                 mOutgoingCall = null;
357             } else {
358                 Log.d(TAG, "Fail to terminate outgoing call " + mOutgoingCall);
359             }
360         } else {
361             Log.w(TAG, "No outgoing call to terminate");
362         }
363     }
364 
365 
366     private final BroadcastReceiver mPickerReceiver = new BroadcastReceiver() {
367         @Override
368         public void onReceive(Context context, Intent intent) {
369             String action = intent.getAction();
370 
371             Log.v(TAG, "mPickerReceiver got " + action);
372 
373             if (BluetoothDevicePicker.ACTION_DEVICE_SELECTED.equals(action)) {
374                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
375                 if (device == null) {
376                     Toast.makeText(getContext(), "No device selected", Toast.LENGTH_SHORT).show();
377                     return;
378                 }
379                 mPickedDevice = device;
380                 String text = device.getName() == null ?
381                     device.getAddress() : device.getName() + " " + device.getAddress();
382                 mPickedDeviceText.setText(text);
383 
384                 // The receiver can now be disabled.
385                 getContext().unregisterReceiver(mPickerReceiver);
386             }
387         }
388     };
389 
390     @Override
onResume()391     public void onResume() {
392         super.onResume();
393         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
394         mBluetoothAdapter.getProfileProxy(
395             getContext(), new ProfileServiceListener(), BluetoothProfile.HEADSET_CLIENT);
396 
397         if (BluetoothConnectionPermissionChecker.isPermissionGranted(
398                 (KitchenSinkActivity) getHost())) {
399             setDevicePickerButtonClickable();
400         } else {
401             setDevicePickerButtonUnclickable();
402         }
403     }
404 
405     @Override
onPause()406     public void onPause() {
407         super.onPause();
408     }
409 
410     class ProfileServiceListener implements BluetoothProfile.ServiceListener {
411         @Override
onServiceConnected(int profile, BluetoothProfile proxy)412         public void onServiceConnected(int profile, BluetoothProfile proxy) {
413             Log.d(TAG, "Proxy connected for profile: " + profile);
414             switch (profile) {
415                 case BluetoothProfile.HEADSET_CLIENT:
416                     mHfpClientProfile = (BluetoothHeadsetClient) proxy;
417                     break;
418                 default:
419                     Log.w(TAG, "onServiceConnected not supported profile: " + profile);
420             }
421         }
422 
423         @Override
onServiceDisconnected(int profile)424         public void onServiceDisconnected(int profile) {
425             Log.d(TAG, "Proxy disconnected for profile: " + profile);
426             switch (profile) {
427                 case BluetoothProfile.HEADSET_CLIENT:
428                     mHfpClientProfile = null;
429                     break;
430                 default:
431                     Log.w(TAG, "onServiceDisconnected not supported profile: " + profile);
432             }
433         }
434     }
435 }
436