1 /*
2  * Copyright 2019 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.android.car.settings.bluetooth;
18 
19 import android.annotation.Nullable;
20 import android.app.ActivityManager;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothDevicePicker;
23 import android.car.drivingstate.CarUxRestrictions;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.os.IBinder;
27 import android.os.RemoteException;
28 import android.text.TextUtils;
29 
30 import androidx.annotation.VisibleForTesting;
31 
32 import com.android.car.settings.R;
33 import com.android.car.settings.common.FragmentController;
34 import com.android.car.settings.common.Logger;
35 import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
36 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
37 
38 /**
39  * Displays a list of Bluetooth devices for the user to select. When a device is selected, a
40  * {@link BluetoothDevicePicker#ACTION_DEVICE_SELECTED} broadcast is sent containing {@link
41  * BluetoothDevice#EXTRA_DEVICE}.
42  *
43  * <p>This is useful to other application to obtain a device without needing to implement the UI.
44  * The activity hosting this controller should be launched with an intent as detailed in {@link
45  * BluetoothDevicePicker#ACTION_LAUNCH}. This controller will filter devices as specified by {@link
46  * BluetoothDevicePicker#EXTRA_FILTER_TYPE} and deliver the broadcast to the specified {@link
47  * BluetoothDevicePicker#EXTRA_LAUNCH_PACKAGE} {@link BluetoothDevicePicker#EXTRA_LAUNCH_CLASS}
48  * component.  If authentication is required ({@link BluetoothDevicePicker#EXTRA_NEED_AUTH}), this
49  * controller will initiate pairing with the device and send the selected broadcast once the device
50  * successfully pairs. If no device is selected and this controller is destroyed, a broadcast with
51  * a {@code null} {@link BluetoothDevice#EXTRA_DEVICE} is sent.
52  */
53 public class BluetoothDevicePickerPreferenceController extends
54         BluetoothScanningDevicesGroupPreferenceController {
55 
56     private static final Logger LOG = new Logger(BluetoothDevicePickerPreferenceController.class);
57 
58     private BluetoothDeviceFilter.Filter mFilter;
59 
60     private boolean mNeedAuth;
61     private String mLaunchPackage;
62     private String mLaunchClass;
63     private String mCallingAppPackageName;
64 
65     private CachedBluetoothDevice mSelectedDevice;
66 
BluetoothDevicePickerPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)67     public BluetoothDevicePickerPreferenceController(Context context, String preferenceKey,
68             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
69         super(context, preferenceKey, fragmentController, uxRestrictions);
70     }
71 
72     /**
73      * Sets the intent with which {@link BluetoothDevicePickerActivity} was launched. The intent
74      * may contain {@link BluetoothDevicePicker} extras to customize the selection list and specify
75      * the destination of the selected device. See {@link BluetoothDevicePicker#ACTION_LAUNCH}.
76      */
setLaunchIntent(Intent intent)77     public void setLaunchIntent(Intent intent) {
78         mNeedAuth = intent.getBooleanExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false);
79         mFilter = BluetoothDeviceFilter.getFilter(
80                 intent.getIntExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE,
81                         BluetoothDevicePicker.FILTER_TYPE_ALL));
82         mLaunchPackage = intent.getStringExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE);
83         mLaunchClass = intent.getStringExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS);
84         mCallingAppPackageName = getCallingAppPackageName(getContext().getActivityToken());
85         if (!TextUtils.equals(mCallingAppPackageName, mLaunchPackage)) {
86             LOG.w("launch package " + mLaunchPackage + " is not equivalent to"
87                     + " calling package " + mCallingAppPackageName);
88         }
89     }
90 
91     @Override
checkInitialized()92     protected void checkInitialized() {
93         if (mFilter == null) {
94             throw new IllegalStateException("launch intent must be set");
95         }
96     }
97 
98     @Override
getDeviceFilter()99     protected BluetoothDeviceFilter.Filter getDeviceFilter() {
100         return mFilter;
101     }
102 
103     @Override
onDeviceClickedInternal(CachedBluetoothDevice cachedDevice)104     protected void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice) {
105         mSelectedDevice = cachedDevice;
106         BluetoothUtils.persistSelectedDeviceInPicker(getContext(), cachedDevice.getAddress());
107 
108         if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED || !mNeedAuth) {
109             sendDevicePickedIntent(cachedDevice.getDevice());
110             getFragmentController().goBack();
111             return;
112         }
113 
114         if (cachedDevice.startPairing()) {
115             LOG.d("startPairing");
116         } else {
117             BluetoothUtils.showError(getContext(), cachedDevice.getName(),
118                     R.string.bluetooth_pairing_error_message);
119             reenableScanning();
120         }
121     }
122 
123     @Override
onStartInternal()124     protected void onStartInternal() {
125         super.onStartInternal();
126         mSelectedDevice = null;
127     }
128 
129     @Override
onDestroyInternal()130     protected void onDestroyInternal() {
131         super.onDestroyInternal();
132         if (mSelectedDevice == null) {
133             // Notify that no device was selected.
134             sendDevicePickedIntent(null);
135         }
136     }
137 
138     @Override
onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState)139     public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
140         super.onDeviceBondStateChanged(cachedDevice, bondState);
141         if (bondState == BluetoothDevice.BOND_BONDED && cachedDevice.equals(mSelectedDevice)) {
142             sendDevicePickedIntent(mSelectedDevice.getDevice());
143             getFragmentController().goBack();
144         }
145     }
146 
sendDevicePickedIntent(BluetoothDevice device)147     private void sendDevicePickedIntent(BluetoothDevice device) {
148         LOG.d("sendDevicePickedIntent device: " + device + " package: " + mLaunchPackage
149                 + " class: " + mLaunchClass);
150         Intent intent = new Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
151         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
152         if (mLaunchPackage != null && mLaunchClass != null) {
153             if (TextUtils.equals(mCallingAppPackageName, mLaunchPackage)) {
154                 intent.setClassName(mLaunchPackage, mLaunchClass);
155             }
156         }
157         getContext().sendBroadcast(intent);
158     }
159 
160     /**
161      * Returns the package name which the activity with {@code activityToken} is launched from.
162      */
163     @VisibleForTesting
164     @Nullable
getCallingAppPackageName(IBinder activityToken)165     String getCallingAppPackageName(IBinder activityToken) {
166         if (activityToken == null) {
167             return null;
168         }
169         String pkg = null;
170         try {
171             pkg = ActivityManager.getService().getLaunchedFromPackage(activityToken);
172         } catch (RemoteException e) {
173             LOG.v("Unable to get launched from package", e);
174         }
175         return pkg;
176     }
177 }
178