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