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