/* * Copyright 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.settings.bluetooth; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; import android.app.ActivityManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothManager; import android.car.drivingstate.CarUxRestrictions; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.IBinder; import android.os.RemoteException; import androidx.preference.PreferenceGroup; import com.android.car.settings.common.FragmentController; import com.android.car.settings.common.Logger; import com.android.settingslib.bluetooth.CachedBluetoothDevice; /** * Controller which sets the Bluetooth adapter to discovery mode and begins scanning for * discoverable devices for as long as the preference group is shown. Discovery * and scanning are halted while any device is pairing. Users with the {@link * DISALLOW_CONFIG_BLUETOOTH} restriction cannot scan for devices, so only cached devices will be * shown. */ public abstract class BluetoothScanningDevicesGroupPreferenceController extends BluetoothDevicesGroupPreferenceController { private static final Logger LOG = new Logger( BluetoothScanningDevicesGroupPreferenceController.class); protected final BluetoothAdapter mBluetoothAdapter; private final AlwaysDiscoverable mAlwaysDiscoverable; private final String mCallingAppPackageName; private boolean mIsScanningEnabled; public BluetoothScanningDevicesGroupPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions) { super(context, preferenceKey, fragmentController, uxRestrictions); mBluetoothAdapter = getContext().getSystemService(BluetoothManager.class).getAdapter(); mAlwaysDiscoverable = new AlwaysDiscoverable(context, mBluetoothAdapter); mCallingAppPackageName = getCallingAppPackageName(getContext().getActivityToken()); } @Override protected final void onDeviceClicked(CachedBluetoothDevice cachedDevice) { LOG.d("onDeviceClicked: " + cachedDevice); disableScanning(); onDeviceClickedInternal(cachedDevice); } /** * Called when the user selects a device in the group. * * @param cachedDevice the device represented by the selected preference. */ protected abstract void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice); @Override protected void onStartInternal() { super.onStartInternal(); mIsScanningEnabled = true; } @Override protected void onStopInternal() { super.onStopInternal(); disableScanning(); getBluetoothManager().getCachedDeviceManager().clearNonBondedDevices(); getPreferenceMap().clear(); getPreference().removeAll(); } @Override protected void updateState(PreferenceGroup preferenceGroup) { super.updateState(preferenceGroup); if (shouldEnableScanning() && mIsScanningEnabled) { enableScanning(); } else { disableScanning(); } } @Override protected boolean shouldShowDisconnectedStateSubtitle() { return false; } protected void reenableScanning() { if (isStarted()) { mIsScanningEnabled = true; } refreshUi(); } private boolean shouldEnableScanning() { for (CachedBluetoothDevice device : getPreferenceMap().keySet()) { if (device.getBondState() == BluetoothDevice.BOND_BONDING) { return false; } } // Users who cannot configure Bluetooth cannot scan. return !getUserManager().hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH); } /** * Starts scanning for devices which will be displayed in the group for a user to select. * Calls are idempotent. */ private void enableScanning() { mIsScanningEnabled = true; if (!mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.startDiscovery(); } if (BluetoothUtils.shouldEnableBTScanning(getContext(), mCallingAppPackageName)) { mAlwaysDiscoverable.start(); } else { LOG.d("Not enabling bluetooth scanning. Calling application " + mCallingAppPackageName + " is not Settings or SystemUi"); } getPreference().setEnabled(true); } /** Stops scanning for devices and disables interaction. Calls are idempotent. */ private void disableScanning() { mIsScanningEnabled = false; getPreference().setEnabled(false); mAlwaysDiscoverable.stop(); if (mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); } } @Override public void onScanningStateChanged(boolean started) { LOG.d("onScanningStateChanged started: " + started + " mIsScanningEnabled: " + mIsScanningEnabled); if (!started && mIsScanningEnabled) { enableScanning(); } } @Override public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { LOG.d("onDeviceBondStateChanged device: " + cachedDevice + " state: " + bondState); if (bondState == BluetoothDevice.BOND_NONE && isStarted()) { mIsScanningEnabled = true; } refreshUi(); } private String getCallingAppPackageName(IBinder activityToken) { String pkg = null; try { pkg = ActivityManager.getService().getLaunchedFromPackage(activityToken); } catch (RemoteException e) { LOG.e("Could not talk to activity manager.", e); } return pkg; } /** * Helper class to keep the {@link BluetoothAdapter} in discoverable mode indefinitely. By * default, setting the scan mode to BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE will * timeout, but for pairing, we want to keep the device discoverable as long as the page is * scanning. */ private static final class AlwaysDiscoverable extends BroadcastReceiver { private final Context mContext; private final BluetoothAdapter mAdapter; private final IntentFilter mIntentFilter = new IntentFilter( BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); private boolean mStarted; AlwaysDiscoverable(Context context, BluetoothAdapter adapter) { mContext = context; mAdapter = adapter; } /** * Sets the adapter scan mode to * {@link BluetoothAdapter#SCAN_MODE_CONNECTABLE_DISCOVERABLE}. {@link #start()} calls * should have a matching calls to {@link #stop()} when discover mode is no longer needed. */ void start() { if (mStarted) { return; } mContext.registerReceiver(this, mIntentFilter); mStarted = true; setDiscoverable(); } void stop() { if (!mStarted) { return; } mContext.unregisterReceiver(this); mStarted = false; mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); } @Override public void onReceive(Context context, Intent intent) { setDiscoverable(); } private void setDiscoverable() { if (mAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { mAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); } } } }