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