• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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