1 /*
2  * Copyright 2018 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.CallSuper;
20 import android.bluetooth.BluetoothAdapter;
21 import android.car.drivingstate.CarUxRestrictions;
22 import android.content.Context;
23 import android.os.UserManager;
24 
25 import androidx.annotation.VisibleForTesting;
26 import androidx.preference.Preference;
27 import androidx.preference.PreferenceGroup;
28 
29 import com.android.car.settings.common.FragmentController;
30 import com.android.settingslib.bluetooth.BluetoothCallback;
31 import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
32 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
33 import com.android.settingslib.bluetooth.LocalBluetoothManager;
34 
35 import java.util.Collection;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.Map;
39 import java.util.Set;
40 
41 /**
42  * Manages a group of Bluetooth devices by adding preferences for devices that pass a subclass
43  * defined filter and removing preferences for devices that no longer pass. Subclasses are
44  * dispatched click events on individual preferences to customize the behavior.
45  *
46  * <p>Note: {@link #refreshUi()} is called whenever a device is added or removed with {@link
47  * #onDeviceAdded(CachedBluetoothDevice)} or {@link #onDeviceDeleted(CachedBluetoothDevice)}.
48  * Subclasses should listen to state changes (and possibly override additional {@link
49  * BluetoothCallback} methods) and call {@link #refreshUi()} for changes which affect their
50  * implementation of {@link #getDeviceFilter()}.
51  */
52 public abstract class BluetoothDevicesGroupPreferenceController extends
53         BluetoothPreferenceController<PreferenceGroup> {
54 
55     private final Map<CachedBluetoothDevice, BluetoothDevicePreference> mPreferenceMap =
56             new HashMap<>();
57     private final Preference.OnPreferenceClickListener mDevicePreferenceClickListener =
58             preference -> {
59                 onDeviceClicked(((BluetoothDevicePreference) preference).getCachedDevice());
60                 return true;
61             };
62 
BluetoothDevicesGroupPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)63     public BluetoothDevicesGroupPreferenceController(Context context, String preferenceKey,
64             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
65         super(context, preferenceKey, fragmentController, uxRestrictions);
66     }
67 
68     @VisibleForTesting
BluetoothDevicesGroupPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, LocalBluetoothManager localBluetoothManager, UserManager userManager)69     BluetoothDevicesGroupPreferenceController(Context context, String preferenceKey,
70             FragmentController fragmentController, CarUxRestrictions uxRestrictions,
71             LocalBluetoothManager localBluetoothManager, UserManager userManager) {
72         super(context, preferenceKey, fragmentController, uxRestrictions, localBluetoothManager,
73                 userManager);
74     }
75 
76     @Override
getPreferenceType()77     protected Class<PreferenceGroup> getPreferenceType() {
78         return PreferenceGroup.class;
79     }
80 
81     /**
82      * Returns a filter for which devices should be included in the group. Devices that do not
83      * pass the filter will not be added. Added devices that no longer pass the filter will be
84      * removed.
85      */
getDeviceFilter()86     protected abstract BluetoothDeviceFilter.Filter getDeviceFilter();
87 
88     /**
89      * Returns a newly created {@link BluetoothDevicePreference} for the given {@link
90      * CachedBluetoothDevice}. Subclasses may override this method to customize how devices are
91      * represented in the group.
92      */
createDevicePreference(CachedBluetoothDevice cachedDevice)93     protected BluetoothDevicePreference createDevicePreference(CachedBluetoothDevice cachedDevice) {
94         return new BluetoothDevicePreference(getContext(), cachedDevice,
95                 shouldShowDisconnectedStateSubtitle());
96     }
97 
98     /**
99      * Returns whether or not the created bluetooth device preferences should display the
100      * disconnected state subtitle.
101      */
shouldShowDisconnectedStateSubtitle()102     protected boolean shouldShowDisconnectedStateSubtitle() {
103         return true;
104     }
105 
106     /**
107      * Called when a preference in the group is clicked.
108      *
109      * @param cachedDevice the device represented by the clicked preference.
110      */
onDeviceClicked(CachedBluetoothDevice cachedDevice)111     protected abstract void onDeviceClicked(CachedBluetoothDevice cachedDevice);
112 
113     /**
114      * Returns a mapping of all {@link CachedBluetoothDevice} instances represented by this group
115      * and their associated preferences.
116      */
getPreferenceMap()117     protected Map<CachedBluetoothDevice, BluetoothDevicePreference> getPreferenceMap() {
118         return mPreferenceMap;
119     }
120 
121     @Override
122     @CallSuper
updateState(PreferenceGroup preferenceGroup)123     protected void updateState(PreferenceGroup preferenceGroup) {
124         Collection<CachedBluetoothDevice> cachedDevices =
125                 getBluetoothManager().getCachedDeviceManager().getCachedDevicesCopy();
126 
127         Set<CachedBluetoothDevice> devicesToRemove = new HashSet<>(mPreferenceMap.keySet());
128         devicesToRemove.removeAll(cachedDevices);
129         for (CachedBluetoothDevice deviceToRemove : devicesToRemove) {
130             removePreference(deviceToRemove);
131         }
132 
133         for (CachedBluetoothDevice cachedDevice : cachedDevices) {
134             if (getDeviceFilter().matches(cachedDevice.getDevice())) {
135                 addPreference(cachedDevice);
136             } else {
137                 removePreference(cachedDevice);
138             }
139         }
140 
141         preferenceGroup.setVisible(preferenceGroup.getPreferenceCount() > 0);
142     }
143 
144     @Override
onBluetoothStateChanged(int bluetoothState)145     public final void onBluetoothStateChanged(int bluetoothState) {
146         super.onBluetoothStateChanged(bluetoothState);
147         if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) {
148             // Cleanup the UI so that we don't have stale representations when the adapter turns
149             // on again. This can happen if Bluetooth crashes and restarts.
150             getPreference().removeAll();
151             mPreferenceMap.clear();
152         }
153     }
154 
155     @Override
onDeviceAdded(CachedBluetoothDevice cachedDevice)156     public final void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
157         refreshUi();
158     }
159 
160     @Override
onDeviceDeleted(CachedBluetoothDevice cachedDevice)161     public final void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
162         refreshUi();
163     }
164 
addPreference(CachedBluetoothDevice cachedDevice)165     private void addPreference(CachedBluetoothDevice cachedDevice) {
166         if (!mPreferenceMap.containsKey(cachedDevice)) {
167             BluetoothDevicePreference devicePreference = createDevicePreference(cachedDevice);
168             devicePreference.setOnPreferenceClickListener(mDevicePreferenceClickListener);
169             mPreferenceMap.put(cachedDevice, devicePreference);
170             getPreference().addPreference(devicePreference);
171         }
172     }
173 
removePreference(CachedBluetoothDevice cachedDevice)174     private void removePreference(CachedBluetoothDevice cachedDevice) {
175         if (mPreferenceMap.containsKey(cachedDevice)) {
176             getPreference().removePreference(mPreferenceMap.get(cachedDevice));
177             mPreferenceMap.remove(cachedDevice);
178         }
179     }
180 }
181