• 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.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