/* * Copyright (C) 2017 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.settings.bluetooth; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.os.Bundle; import android.util.Log; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.connecteddevice.DevicePreferenceCallback; import com.android.settings.core.SubSettingLauncher; import com.android.settings.overlay.FeatureFactory; import com.android.settings.widget.GearPreference; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Update the bluetooth devices. It gets bluetooth event from {@link LocalBluetoothManager} using * {@link BluetoothCallback}. It notifies the upper level whether to add/remove the preference * through {@link DevicePreferenceCallback} * * In {@link BluetoothDeviceUpdater}, it uses {@link #isFilterMatched(CachedBluetoothDevice)} to * detect whether the {@link CachedBluetoothDevice} is relevant. */ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, LocalBluetoothProfileManager.ServiceListener { protected final MetricsFeatureProvider mMetricsFeatureProvider; protected final DevicePreferenceCallback mDevicePreferenceCallback; protected final Map mPreferenceMap; protected Context mContext; protected Context mPrefContext; @VisibleForTesting protected LocalBluetoothManager mLocalManager; protected int mMetricsCategory; protected static final String TAG = "BluetoothDeviceUpdater"; private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); @VisibleForTesting final GearPreference.OnGearClickListener mDeviceProfilesListener = pref -> { launchDeviceDetails(pref); }; public BluetoothDeviceUpdater(Context context, DevicePreferenceCallback devicePreferenceCallback, int metricsCategory) { this(context, devicePreferenceCallback, Utils.getLocalBtManager(context), metricsCategory); } @VisibleForTesting BluetoothDeviceUpdater(Context context, DevicePreferenceCallback devicePreferenceCallback, LocalBluetoothManager localManager, int metricsCategory) { mContext = context; mDevicePreferenceCallback = devicePreferenceCallback; mPreferenceMap = new HashMap<>(); mLocalManager = localManager; mMetricsCategory = metricsCategory; mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); } /** * Register the bluetooth event callback and update the list */ public void registerCallback() { if (mLocalManager == null) { Log.e(getLogTag(), "registerCallback() Bluetooth is not supported on this device"); return; } mLocalManager.setForegroundActivity(mContext); mLocalManager.getEventManager().registerCallback(this); mLocalManager.getProfileManager().addServiceListener(this); forceUpdate(); } /** * Unregister the bluetooth event callback */ public void unregisterCallback() { if (mLocalManager == null) { Log.e(getLogTag(), "unregisterCallback() Bluetooth is not supported on this device"); return; } mLocalManager.setForegroundActivity(null); mLocalManager.getEventManager().unregisterCallback(this); mLocalManager.getProfileManager().removeServiceListener(this); } /** * Force to update the list of bluetooth devices */ public void forceUpdate() { if (mLocalManager == null) { Log.e(getLogTag(), "forceUpdate() Bluetooth is not supported on this device"); return; } if (BluetoothAdapter.getDefaultAdapter().isEnabled()) { final Collection cachedDevices = mLocalManager.getCachedDeviceManager().getCachedDevicesCopy(); for (CachedBluetoothDevice cachedBluetoothDevice : cachedDevices) { update(cachedBluetoothDevice); } } else { removeAllDevicesFromPreference(); } } public void removeAllDevicesFromPreference() { if (mLocalManager == null) { Log.e(getLogTag(), "removeAllDevicesFromPreference() BT is not supported on this device"); return; } final Collection cachedDevices = mLocalManager.getCachedDeviceManager().getCachedDevicesCopy(); for (CachedBluetoothDevice cachedBluetoothDevice : cachedDevices) { removePreference(cachedBluetoothDevice); } } @Override public void onBluetoothStateChanged(int bluetoothState) { if (BluetoothAdapter.STATE_ON == bluetoothState) { forceUpdate(); } else if (BluetoothAdapter.STATE_OFF == bluetoothState) { removeAllDevicesFromPreference(); } } @Override public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { update(cachedDevice); } @Override public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { // Used to combine the hearing aid entries just after pairing. Once both the hearing aids // get connected and their hiSyncId gets populated, this gets called for one of the // 2 hearing aids so that only one entry in the connected devices list will be seen. removePreference(cachedDevice); } @Override public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { update(cachedDevice); } @Override public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile) { if (DBG) { Log.d(getLogTag(), "onProfileConnectionStateChanged() device: " + cachedDevice.getName() + ", state: " + state + ", bluetoothProfile: " + bluetoothProfile); } update(cachedDevice); } @Override public void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { Log.d(getLogTag(), "onAclConnectionStateChanged() device: " + cachedDevice.getName() + ", state: " + state); update(cachedDevice); } @Override public void onServiceConnected() { // When bluetooth service connected update the UI forceUpdate(); } @Override public void onServiceDisconnected() { } /** * Set the context to generate the {@link Preference}, so it could get the correct theme. */ public void setPrefContext(Context context) { mPrefContext = context; } /** * Return {@code true} if {@code cachedBluetoothDevice} matches this * {@link BluetoothDeviceUpdater} and should stay in the list, otherwise return {@code false} */ public abstract boolean isFilterMatched(CachedBluetoothDevice cachedBluetoothDevice); /** * Return a preference key for logging */ protected abstract String getPreferenceKey(); /** * Update whether to show {@link CachedBluetoothDevice} in the list. */ protected void update(CachedBluetoothDevice cachedBluetoothDevice) { if (isFilterMatched(cachedBluetoothDevice)) { // Add the preference if it is new one addPreference(cachedBluetoothDevice); } else { removePreference(cachedBluetoothDevice); } } /** * Add the {@link Preference} that represents the {@code cachedDevice} */ protected void addPreference(CachedBluetoothDevice cachedDevice) { addPreference(cachedDevice, BluetoothDevicePreference.SortType.TYPE_DEFAULT); } /** * Add the {@link Preference} with {@link BluetoothDevicePreference.SortType} that * represents the {@code cachedDevice} */ protected void addPreference(CachedBluetoothDevice cachedDevice, @BluetoothDevicePreference.SortType int type) { final BluetoothDevice device = cachedDevice.getDevice(); if (!mPreferenceMap.containsKey(device)) { BluetoothDevicePreference btPreference = new BluetoothDevicePreference(mPrefContext, cachedDevice, true /* showDeviceWithoutNames */, type); btPreference.setKey(getPreferenceKey()); btPreference.setOnGearClickListener(mDeviceProfilesListener); if (this instanceof Preference.OnPreferenceClickListener) { btPreference.setOnPreferenceClickListener( (Preference.OnPreferenceClickListener) this); } mPreferenceMap.put(device, btPreference); mDevicePreferenceCallback.onDeviceAdded(btPreference); } } /** * Remove the {@link Preference} that represents the {@code cachedDevice} */ protected void removePreference(CachedBluetoothDevice cachedDevice) { final BluetoothDevice device = cachedDevice.getDevice(); final CachedBluetoothDevice subCachedDevice = cachedDevice.getSubDevice(); if (mPreferenceMap.containsKey(device)) { removePreference(device); } else if (subCachedDevice != null) { // When doing remove, to check if preference maps to sub device. // This would happen when connection state is changed in detail page that there is no // callback from SettingsLib. final BluetoothDevice subDevice = subCachedDevice.getDevice(); removePreference(subDevice); } } private void removePreference(BluetoothDevice device) { if (mPreferenceMap.containsKey(device)) { mDevicePreferenceCallback.onDeviceRemoved(mPreferenceMap.get(device)); mPreferenceMap.remove(device); } } /** * Get {@link CachedBluetoothDevice} from {@link Preference} and it is used to init * {@link SubSettingLauncher} to launch {@link BluetoothDeviceDetailsFragment} */ protected void launchDeviceDetails(Preference preference) { mMetricsFeatureProvider.logClickedPreference(preference, mMetricsCategory); final CachedBluetoothDevice device = ((BluetoothDevicePreference) preference).getBluetoothDevice(); if (device == null) { return; } final Bundle args = new Bundle(); args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, device.getDevice().getAddress()); new SubSettingLauncher(mContext) .setDestination(BluetoothDeviceDetailsFragment.class.getName()) .setArguments(args) .setTitleRes(R.string.device_details_title) .setSourceMetricsCategory(mMetricsCategory) .launch(); } /** * @return {@code true} if {@code cachedBluetoothDevice} is connected * and the bond state is bonded. */ public boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) { if (cachedDevice == null) { return false; } final BluetoothDevice device = cachedDevice.getDevice(); if (DBG) { Log.d(getLogTag(), "isDeviceConnected() device name : " + cachedDevice.getName() + ", is connected : " + device.isConnected() + " , is profile connected : " + cachedDevice.isConnected()); } return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected(); } /** * Update the attributes of {@link Preference}. */ public void refreshPreference() { List removeList = new ArrayList<>(); mPreferenceMap.forEach((key, preference) -> { if (isDeviceOfMapInCachedDevicesList(key)) { ((BluetoothDevicePreference) preference).onPreferenceAttributesChanged(); } else { // If the BluetoothDevice of preference is not in the CachedDevices List, then // remove this preference. removeList.add(key); } }); for (BluetoothDevice bluetoothDevice : removeList) { Log.d(getLogTag(), "removePreference key: " + bluetoothDevice.getAnonymizedAddress()); removePreference(bluetoothDevice); } } protected boolean isDeviceInCachedDevicesList(CachedBluetoothDevice cachedDevice) { return mLocalManager.getCachedDeviceManager().getCachedDevicesCopy().contains(cachedDevice); } private boolean isDeviceOfMapInCachedDevicesList(BluetoothDevice inputBluetoothDevice) { Collection cachedDevices = mLocalManager.getCachedDeviceManager().getCachedDevicesCopy(); if (cachedDevices == null || cachedDevices.isEmpty()) { return false; } return cachedDevices.stream() .anyMatch(cachedBluetoothDevice -> cachedBluetoothDevice.getDevice() != null && cachedBluetoothDevice.getDevice().equals(inputBluetoothDevice)); } protected String getLogTag() { return TAG; } }