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.requestPermission(Manifest.permission.BLUETOOTH_CONNECT, 120 this, 121 this::setDevicePickerButtonClickable, 122 () -> { 123 setDevicePickerButtonUnclickable(); 124 Toast.makeText(getContext(), 125 "Device picker can't run without BLUETOOTH_CONNECT permission. " 126 + "(You can change permissions in Settings.)", 127 Toast.LENGTH_SHORT).show(); 128 } 129 ); 130 } 131 } 132 setUpInCallServiceImpl()133 private void setUpInCallServiceImpl() { 134 mInCallServiceConnection = new ServiceConnection() { 135 @Override 136 public void onServiceConnected(ComponentName name, IBinder service) { 137 Log.i(TAG, "InCallServiceImpl is connected"); 138 mInCallService = ((InCallServiceImpl.LocalBinder) service).getService(); 139 } 140 141 @Override 142 public void onServiceDisconnected(ComponentName name) { 143 Log.i(TAG, "InCallServiceImpl is disconnected"); 144 mInCallService = null; 145 } 146 }; 147 148 Intent intent = new Intent(this.getContext(), InCallServiceImpl.class); 149 intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND); 150 this.getContext().bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE); 151 } 152 setDevicePickerButtonClickable()153 private void setDevicePickerButtonClickable() { 154 mDevicePicker.setClickable(true); 155 156 // Pick a bluetooth device 157 mDevicePicker.setOnClickListener(view -> launchDevicePicker()); 158 } 159 setDevicePickerButtonUnclickable()160 private void setDevicePickerButtonUnclickable() { 161 mDevicePicker.setClickable(false); 162 } 163 launchDevicePicker()164 void launchDevicePicker() { 165 IntentFilter filter = new IntentFilter(); 166 filter.addAction(BluetoothDevicePicker.ACTION_DEVICE_SELECTED); 167 getContext().registerReceiver(mPickerReceiver, filter); 168 169 Intent intent = new Intent(DEVICE_PICKER_ACTION); 170 intent.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 171 getContext().startActivity(intent); 172 } 173 connect()174 void connect() { 175 if (mPickedDevice == null) { 176 Log.w(TAG, "Device null when trying to connect sco!"); 177 return; 178 } 179 180 // Check if we have the proxy and connect the device. 181 if (mHfpClientProfile == null) { 182 Log.w(TAG, "HFP Profile proxy not available, cannot connect sco to " + mPickedDevice); 183 return; 184 } 185 mHfpClientProfile.setConnectionPolicy(mPickedDevice, 186 BluetoothProfile.CONNECTION_POLICY_ALLOWED); 187 mPickedDevice.connect(); 188 } 189 connectSco()190 void connectSco() { 191 Call call = getFirstActiveCall(); 192 if (call != null) { 193 // TODO(b/206035301): Use the public version of this string 194 call.sendCallEvent("com.android.bluetooth.hfpclient.SCO_CONNECT", 195 /* extras= */ null); 196 } 197 } 198 disconnectSco()199 void disconnectSco() { 200 Call call = getFirstActiveCall(); 201 if (call != null) { 202 // TODO(b/206035301): Use the public version of this string 203 call.sendCallEvent("com.android.bluetooth.hfpclient.SCO_DISCONNECT", 204 /* extras= */ null); 205 } 206 } 207 holdCall()208 void holdCall() { 209 Call call = getFirstActiveCall(); 210 if (call != null) { 211 call.hold(); 212 } 213 } 214 startCall()215 void startCall() { 216 TelecomManager telecomManager = getContext().getSystemService(TelecomManager.class); 217 if (!Objects.equal(telecomManager.getDefaultDialerPackage(), 218 getContext().getPackageName())) { 219 Log.w(TAG, "Kitchen Sink cannot manage phone calls unless it is the default " 220 + "dialer app. This can be set in Settings>Apps>Default apps"); 221 } 222 223 Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL, mOutgoingPhoneNumber.getText().toString(), 224 /* fragment= */ null); 225 telecomManager.placeCall(uri, /* extras= */ null); 226 } 227 stopCall()228 void stopCall() { 229 Call call = getFirstActiveCall(); 230 if (call != null) { 231 call.disconnect(); 232 } 233 } 234 getFirstActiveCall()235 private Call getFirstActiveCall() { 236 if (mInCallService == null) { 237 Log.w(TAG, "InCallServiceImpl was not connected"); 238 return null; 239 } 240 241 List<Call> calls = mInCallService.getCalls(); 242 if (calls == null || calls.size() == 0) { 243 Log.w(TAG, "No calls are currently connected"); 244 return null; 245 } 246 247 return mInCallService.getCalls().get(0); 248 } 249 250 251 private final BroadcastReceiver mPickerReceiver = new BroadcastReceiver() { 252 @Override 253 public void onReceive(Context context, Intent intent) { 254 String action = intent.getAction(); 255 256 Log.v(TAG, "mPickerReceiver got " + action); 257 258 if (BluetoothDevicePicker.ACTION_DEVICE_SELECTED.equals(action)) { 259 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 260 if (device == null) { 261 Toast.makeText(getContext(), "No device selected", Toast.LENGTH_SHORT).show(); 262 return; 263 } 264 mPickedDevice = device; 265 String text = device.getName() == null ? 266 device.getAddress() : device.getName() + " " + device.getAddress(); 267 mPickedDeviceText.setText(text); 268 269 // The receiver can now be disabled. 270 getContext().unregisterReceiver(mPickerReceiver); 271 } 272 } 273 }; 274 275 @Override onResume()276 public void onResume() { 277 super.onResume(); 278 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 279 mBluetoothAdapter.getProfileProxy( 280 getContext(), new ProfileServiceListener(), BluetoothProfile.HEADSET_CLIENT); 281 282 if (BluetoothPermissionChecker.isPermissionGranted( 283 getActivity(), Manifest.permission.BLUETOOTH_CONNECT)) { 284 setDevicePickerButtonClickable(); 285 } else { 286 setDevicePickerButtonUnclickable(); 287 } 288 } 289 290 @Override onDestroy()291 public void onDestroy() { 292 getContext().unbindService(mInCallServiceConnection); 293 super.onDestroy(); 294 } 295 296 class ProfileServiceListener implements BluetoothProfile.ServiceListener { 297 @Override onServiceConnected(int profile, BluetoothProfile proxy)298 public void onServiceConnected(int profile, BluetoothProfile proxy) { 299 Log.d(TAG, "Proxy connected for profile: " + profile); 300 switch (profile) { 301 case BluetoothProfile.HEADSET_CLIENT: 302 mHfpClientProfile = (BluetoothHeadsetClient) proxy; 303 break; 304 default: 305 Log.w(TAG, "onServiceConnected not supported profile: " + profile); 306 } 307 } 308 309 @Override onServiceDisconnected(int profile)310 public void onServiceDisconnected(int profile) { 311 Log.d(TAG, "Proxy disconnected for profile: " + profile); 312 switch (profile) { 313 case BluetoothProfile.HEADSET_CLIENT: 314 mHfpClientProfile = null; 315 break; 316 default: 317 Log.w(TAG, "onServiceDisconnected not supported profile: " + profile); 318 } 319 } 320 } 321 } 322