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.Manifest; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothDevicePicker; 23 import android.bluetooth.BluetoothHeadsetClient; 24 import android.bluetooth.BluetoothProfile; 25 import android.content.BroadcastReceiver; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.content.ServiceConnection; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.os.IBinder; 34 import android.telecom.Call; 35 import android.telecom.PhoneAccount; 36 import android.telecom.TelecomManager; 37 import android.util.Log; 38 import android.view.LayoutInflater; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.widget.Button; 42 import android.widget.EditText; 43 import android.widget.TextView; 44 import android.widget.Toast; 45 46 import androidx.annotation.Nullable; 47 import androidx.fragment.app.Fragment; 48 49 import com.google.android.car.kitchensink.KitchenSinkActivity; 50 import com.google.android.car.kitchensink.R; 51 import com.google.common.base.Objects; 52 53 import java.util.List; 54 55 public class BluetoothHeadsetFragment extends Fragment { 56 private static final String TAG = "CAR.BLUETOOTH.KS"; 57 BluetoothAdapter mBluetoothAdapter; 58 BluetoothDevice mPickedDevice; 59 60 TextView mPickedDeviceText; 61 Button mDevicePicker; 62 Button mConnect; 63 Button mScoConnect; 64 Button mScoDisconnect; 65 Button mHoldCall; 66 Button mStartOutgoingCall; 67 Button mEndOutgoingCall; 68 EditText mOutgoingPhoneNumber; 69 70 BluetoothHeadsetClient mHfpClientProfile; 71 InCallServiceImpl mInCallService; 72 ServiceConnection mInCallServiceConnection; 73 74 // Intent for picking a Bluetooth device 75 public static final String DEVICE_PICKER_ACTION = 76 "android.bluetooth.devicepicker.action.LAUNCH"; 77 78 @Override onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)79 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 80 @Nullable Bundle savedInstanceState) { 81 View v = inflater.inflate(R.layout.bluetooth_headset, container, false); 82 83 mPickedDeviceText = (TextView) v.findViewById(R.id.bluetooth_device); 84 mDevicePicker = (Button) v.findViewById(R.id.bluetooth_pick_device); 85 mConnect = (Button) v.findViewById(R.id.bluetooth_headset_connect); 86 mScoConnect = (Button) v.findViewById(R.id.bluetooth_sco_connect); 87 mScoDisconnect = (Button) v.findViewById(R.id.bluetooth_sco_disconnect); 88 mHoldCall = (Button) v.findViewById(R.id.bluetooth_hold_call); 89 mStartOutgoingCall = (Button) v.findViewById(R.id.bluetooth_start_outgoing_call); 90 mEndOutgoingCall = (Button) v.findViewById(R.id.bluetooth_end_outgoing_call); 91 mOutgoingPhoneNumber = (EditText) v.findViewById(R.id.bluetooth_outgoing_phone_number); 92 93 checkPermissions(); 94 setUpInCallServiceImpl(); 95 96 // Connect profile 97 mConnect.setOnClickListener(view -> connect()); 98 99 // Connect SCO 100 mScoConnect.setOnClickListener(view -> connectSco()); 101 102 // Disconnect SCO 103 mScoDisconnect.setOnClickListener(view -> disconnectSco()); 104 105 // Place the current call on hold 106 mHoldCall.setOnClickListener(view -> holdCall()); 107 108 // Start an outgoing call 109 mStartOutgoingCall.setOnClickListener(view -> startCall()); 110 111 // Stop an outgoing call 112 mEndOutgoingCall.setOnClickListener(view -> stopCall()); 113 114 return v; 115 } 116 checkPermissions()117 private void checkPermissions() { 118 if (!BluetoothPermissionChecker.isPermissionGranted( 119 (KitchenSinkActivity) getHost(), Manifest.permission.BLUETOOTH_CONNECT)) { 120 BluetoothPermissionChecker.requestPermission(Manifest.permission.BLUETOOTH_CONNECT, 121 this, 122 this::setDevicePickerButtonClickable, 123 () -> { 124 setDevicePickerButtonUnclickable(); 125 Toast.makeText(getContext(), 126 "Device picker can't run without BLUETOOTH_CONNECT permission. " 127 + "(You can change permissions in Settings.)", 128 Toast.LENGTH_SHORT).show(); 129 } 130 ); 131 } 132 } 133 setUpInCallServiceImpl()134 private void setUpInCallServiceImpl() { 135 mInCallServiceConnection = new ServiceConnection() { 136 @Override 137 public void onServiceConnected(ComponentName name, IBinder service) { 138 Log.i(TAG, "InCallServiceImpl is connected"); 139 mInCallService = ((InCallServiceImpl.LocalBinder) service).getService(); 140 } 141 142 @Override 143 public void onServiceDisconnected(ComponentName name) { 144 Log.i(TAG, "InCallServiceImpl is disconnected"); 145 mInCallService = null; 146 } 147 }; 148 149 Intent intent = new Intent(this.getContext(), InCallServiceImpl.class); 150 intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND); 151 this.getContext().bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE); 152 } 153 setDevicePickerButtonClickable()154 private void setDevicePickerButtonClickable() { 155 mDevicePicker.setClickable(true); 156 157 // Pick a bluetooth device 158 mDevicePicker.setOnClickListener(view -> launchDevicePicker()); 159 } 160 setDevicePickerButtonUnclickable()161 private void setDevicePickerButtonUnclickable() { 162 mDevicePicker.setClickable(false); 163 } 164 launchDevicePicker()165 void launchDevicePicker() { 166 IntentFilter filter = new IntentFilter(); 167 filter.addAction(BluetoothDevicePicker.ACTION_DEVICE_SELECTED); 168 getContext().registerReceiver(mPickerReceiver, filter); 169 170 Intent intent = new Intent(DEVICE_PICKER_ACTION); 171 intent.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 172 getContext().startActivity(intent); 173 } 174 connect()175 void connect() { 176 if (mPickedDevice == null) { 177 Log.w(TAG, "Device null when trying to connect sco!"); 178 return; 179 } 180 181 // Check if we have the proxy and connect the device. 182 if (mHfpClientProfile == null) { 183 Log.w(TAG, "HFP Profile proxy not available, cannot connect sco to " + mPickedDevice); 184 return; 185 } 186 mHfpClientProfile.setConnectionPolicy(mPickedDevice, 187 BluetoothProfile.CONNECTION_POLICY_ALLOWED); 188 mPickedDevice.connect(); 189 } 190 connectSco()191 void connectSco() { 192 Call call = getFirstActiveCall(); 193 if (call != null) { 194 // TODO(b/206035301): Use the public version of this string 195 call.sendCallEvent("com.android.bluetooth.hfpclient.SCO_CONNECT", 196 /* extras= */ null); 197 } 198 } 199 disconnectSco()200 void disconnectSco() { 201 Call call = getFirstActiveCall(); 202 if (call != null) { 203 // TODO(b/206035301): Use the public version of this string 204 call.sendCallEvent("com.android.bluetooth.hfpclient.SCO_DISCONNECT", 205 /* extras= */ null); 206 } 207 } 208 holdCall()209 void holdCall() { 210 Call call = getFirstActiveCall(); 211 if (call != null) { 212 call.hold(); 213 } 214 } 215 startCall()216 void startCall() { 217 TelecomManager telecomManager = getContext().getSystemService(TelecomManager.class); 218 if (!Objects.equal(telecomManager.getDefaultDialerPackage(), 219 getContext().getPackageName())) { 220 Log.w(TAG, "Kitchen Sink cannot manage phone calls unless it is the default " 221 + "dialer app. This can be set in Settings>Apps>Default apps"); 222 } 223 224 Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, mOutgoingPhoneNumber.getText().toString(), 225 /* fragment= */ null); 226 telecomManager.placeCall(uri, /* extras= */ null); 227 } 228 stopCall()229 void stopCall() { 230 Call call = getFirstActiveCall(); 231 if (call != null) { 232 call.disconnect(); 233 } 234 } 235 getFirstActiveCall()236 private Call getFirstActiveCall() { 237 if (mInCallService == null) { 238 Log.w(TAG, "InCallServiceImpl was not connected"); 239 return null; 240 } 241 242 List<Call> calls = mInCallService.getCalls(); 243 if (calls == null || calls.size() == 0) { 244 Log.w(TAG, "No calls are currently connected"); 245 return null; 246 } 247 248 return mInCallService.getCalls().get(0); 249 } 250 251 252 private final BroadcastReceiver mPickerReceiver = new BroadcastReceiver() { 253 @Override 254 public void onReceive(Context context, Intent intent) { 255 String action = intent.getAction(); 256 257 Log.v(TAG, "mPickerReceiver got " + action); 258 259 if (BluetoothDevicePicker.ACTION_DEVICE_SELECTED.equals(action)) { 260 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 261 if (device == null) { 262 Toast.makeText(getContext(), "No device selected", Toast.LENGTH_SHORT).show(); 263 return; 264 } 265 mPickedDevice = device; 266 String text = device.getName() == null ? 267 device.getAddress() : device.getName() + " " + device.getAddress(); 268 mPickedDeviceText.setText(text); 269 270 // The receiver can now be disabled. 271 getContext().unregisterReceiver(mPickerReceiver); 272 } 273 } 274 }; 275 276 @Override onResume()277 public void onResume() { 278 super.onResume(); 279 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 280 mBluetoothAdapter.getProfileProxy( 281 getContext(), new ProfileServiceListener(), BluetoothProfile.HEADSET_CLIENT); 282 283 if (BluetoothPermissionChecker.isPermissionGranted( 284 (KitchenSinkActivity) getHost(), Manifest.permission.BLUETOOTH_CONNECT)) { 285 setDevicePickerButtonClickable(); 286 } else { 287 setDevicePickerButtonUnclickable(); 288 } 289 } 290 291 @Override onPause()292 public void onPause() { 293 super.onPause(); 294 } 295 296 @Override onDestroy()297 public void onDestroy() { 298 getContext().unbindService(mInCallServiceConnection); 299 super.onDestroy(); 300 } 301 302 class ProfileServiceListener implements BluetoothProfile.ServiceListener { 303 @Override onServiceConnected(int profile, BluetoothProfile proxy)304 public void onServiceConnected(int profile, BluetoothProfile proxy) { 305 Log.d(TAG, "Proxy connected for profile: " + profile); 306 switch (profile) { 307 case BluetoothProfile.HEADSET_CLIENT: 308 mHfpClientProfile = (BluetoothHeadsetClient) proxy; 309 break; 310 default: 311 Log.w(TAG, "onServiceConnected not supported profile: " + profile); 312 } 313 } 314 315 @Override onServiceDisconnected(int profile)316 public void onServiceDisconnected(int profile) { 317 Log.d(TAG, "Proxy disconnected for profile: " + profile); 318 switch (profile) { 319 case BluetoothProfile.HEADSET_CLIENT: 320 mHfpClientProfile = null; 321 break; 322 default: 323 Log.w(TAG, "onServiceDisconnected not supported profile: " + profile); 324 } 325 } 326 } 327 } 328