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