• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 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.settings.homepage.contextualcards.slices;
18 
19 import android.app.PendingIntent;
20 import android.app.settings.SettingsEnums;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.graphics.drawable.Drawable;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.util.Log;
29 import android.util.Pair;
30 
31 import androidx.core.graphics.drawable.IconCompat;
32 import androidx.slice.Slice;
33 import androidx.slice.builders.ListBuilder;
34 import androidx.slice.builders.SliceAction;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.settings.R;
38 import com.android.settings.SubSettings;
39 import com.android.settings.Utils;
40 import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
41 import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment;
42 import com.android.settings.bluetooth.BluetoothPairingDetail;
43 import com.android.settings.bluetooth.SavedBluetoothDeviceUpdater;
44 import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment;
45 import com.android.settings.core.SubSettingLauncher;
46 import com.android.settings.slices.CustomSliceRegistry;
47 import com.android.settings.slices.CustomSliceable;
48 import com.android.settings.slices.SliceBroadcastReceiver;
49 import com.android.settings.slices.SliceBuilderUtils;
50 import com.android.settingslib.bluetooth.BluetoothUtils;
51 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
52 import com.android.settingslib.bluetooth.LocalBluetoothManager;
53 
54 import java.util.ArrayList;
55 import java.util.Collection;
56 import java.util.Comparator;
57 import java.util.List;
58 import java.util.stream.Collectors;
59 
60 public class BluetoothDevicesSlice implements CustomSliceable {
61 
62     @VisibleForTesting
63     static final String BLUETOOTH_DEVICE_HASH_CODE = "bluetooth_device_hash_code";
64 
65     @VisibleForTesting
66     static final int DEFAULT_EXPANDED_ROW_COUNT = 2;
67 
68     @VisibleForTesting
69     static final String EXTRA_ENABLE_BLUETOOTH = "enable_bluetooth";
70 
71     /**
72      * Refer {@link com.android.settings.bluetooth.BluetoothDevicePreference#compareTo} to sort the
73      * Bluetooth devices by {@link CachedBluetoothDevice}.
74      */
75     private static final Comparator<CachedBluetoothDevice> COMPARATOR = Comparator.naturalOrder();
76 
77     private static final String TAG = "BluetoothDevicesSlice";
78 
79     // For seamless UI transition after tapping this slice to enable Bluetooth, this flag is to
80     // update the layout promptly since it takes time for Bluetooth to reflect the enabling state.
81     private static boolean sBluetoothEnabling;
82 
83     private final Context mContext;
84     private AvailableMediaBluetoothDeviceUpdater mAvailableMediaBtDeviceUpdater;
85     private SavedBluetoothDeviceUpdater mSavedBtDeviceUpdater;
86 
BluetoothDevicesSlice(Context context)87     public BluetoothDevicesSlice(Context context) {
88         mContext = context;
89         BluetoothUpdateWorker.initLocalBtManager(context);
90     }
91 
92     @Override
getUri()93     public Uri getUri() {
94         return CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI;
95     }
96 
97     @Override
getSlice()98     public Slice getSlice() {
99         final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
100         if (btAdapter == null) {
101             Log.i(TAG, "Bluetooth is not supported on this hardware platform");
102             return null;
103         }
104 
105         final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY)
106                 .setAccentColor(COLOR_NOT_TINTED);
107 
108         // Only show this header when Bluetooth is off and not turning on.
109         if (!isBluetoothEnabled(btAdapter) && !sBluetoothEnabling) {
110             return listBuilder.addRow(getBluetoothOffHeader()).build();
111         }
112 
113         // Always reset this flag when showing the layout of Bluetooth on
114         sBluetoothEnabling = false;
115 
116         // Add the header of Bluetooth on
117         listBuilder.addRow(getBluetoothOnHeader());
118 
119         // Add row builders of Bluetooth devices.
120         getBluetoothRowBuilders().forEach(row -> listBuilder.addRow(row));
121 
122         return listBuilder.build();
123     }
124 
125     @Override
getIntent()126     public Intent getIntent() {
127         final String screenTitle = mContext.getText(R.string.connected_devices_dashboard_title)
128                 .toString();
129 
130         return SliceBuilderUtils.buildSearchResultPageIntent(mContext,
131                 ConnectedDeviceDashboardFragment.class.getName(), "" /* key */,
132                 screenTitle,
133                 SettingsEnums.SLICE)
134                 .setClassName(mContext.getPackageName(), SubSettings.class.getName())
135                 .setData(getUri());
136     }
137 
138     @Override
onNotifyChange(Intent intent)139     public void onNotifyChange(Intent intent) {
140         final boolean enableBluetooth = intent.getBooleanExtra(EXTRA_ENABLE_BLUETOOTH, false);
141         if (enableBluetooth) {
142             final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
143             if (!isBluetoothEnabled(btAdapter)) {
144                 sBluetoothEnabling = true;
145                 btAdapter.enable();
146                 mContext.getContentResolver().notifyChange(getUri(), null);
147             }
148             return;
149         }
150 
151         final int bluetoothDeviceHashCode = intent.getIntExtra(BLUETOOTH_DEVICE_HASH_CODE, -1);
152         for (CachedBluetoothDevice device : getPairedBluetoothDevices()) {
153             if (device.hashCode() == bluetoothDeviceHashCode) {
154                 if (device.isConnected()) {
155                     device.setActive();
156                 } else if (!device.isBusy()) {
157                     device.connect();
158                 }
159                 return;
160             }
161         }
162     }
163 
164     @Override
getBackgroundWorkerClass()165     public Class getBackgroundWorkerClass() {
166         return BluetoothUpdateWorker.class;
167     }
168 
169     @VisibleForTesting
getPairedBluetoothDevices()170     List<CachedBluetoothDevice> getPairedBluetoothDevices() {
171         final List<CachedBluetoothDevice> bluetoothDeviceList = new ArrayList<>();
172 
173         // If Bluetooth is disable, skip getting the Bluetooth devices.
174         if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
175             Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is disabled.");
176             return bluetoothDeviceList;
177         }
178 
179         final LocalBluetoothManager localBtManager = BluetoothUpdateWorker.getLocalBtManager();
180         if (localBtManager == null) {
181             Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is not ready.");
182             return bluetoothDeviceList;
183         }
184 
185         final Collection<CachedBluetoothDevice> cachedDevices =
186                 localBtManager.getCachedDeviceManager().getCachedDevicesCopy();
187 
188         // Get all paired devices and sort them.
189         return cachedDevices.stream()
190                 .filter(device -> device.getDevice().getBondState() == BluetoothDevice.BOND_BONDED)
191                 .sorted(COMPARATOR).collect(Collectors.toList());
192     }
193 
194     @VisibleForTesting
getBluetoothDetailIntent(CachedBluetoothDevice device)195     PendingIntent getBluetoothDetailIntent(CachedBluetoothDevice device) {
196         final Bundle args = new Bundle();
197         args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS,
198                 device.getDevice().getAddress());
199         final SubSettingLauncher subSettingLauncher = new SubSettingLauncher(mContext);
200         subSettingLauncher.setDestination(BluetoothDeviceDetailsFragment.class.getName())
201                 .setArguments(args)
202                 .setTitleRes(R.string.device_details_title)
203                 .setSourceMetricsCategory(SettingsEnums.BLUETOOTH_DEVICE_DETAILS);
204 
205         // The requestCode should be unique, use the hashcode of device as request code.
206         return PendingIntent
207                 .getActivity(mContext, device.hashCode() /* requestCode */,
208                         subSettingLauncher.toIntent(),
209                         0  /* flags */);
210     }
211 
212     @VisibleForTesting
getBluetoothDeviceIcon(CachedBluetoothDevice device)213     IconCompat getBluetoothDeviceIcon(CachedBluetoothDevice device) {
214         final Pair<Drawable, String> pair =
215                 BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, device);
216         final Drawable drawable = pair.first;
217 
218         // Use default Bluetooth icon if we can't get one.
219         if (drawable == null) {
220             return IconCompat.createWithResource(mContext,
221                     com.android.internal.R.drawable.ic_settings_bluetooth);
222         }
223 
224         return Utils.createIconWithDrawable(drawable);
225     }
226 
getBluetoothOffHeader()227     private ListBuilder.RowBuilder getBluetoothOffHeader() {
228         final Drawable drawable = mContext.getDrawable(R.drawable.ic_bluetooth_disabled);
229         final int tint = Utils.getDisabled(mContext, Utils.getColorAttrDefaultColor(mContext,
230                 android.R.attr.colorControlNormal));
231         drawable.setTint(tint);
232         final IconCompat icon = Utils.createIconWithDrawable(drawable);
233         final CharSequence title = mContext.getText(R.string.bluetooth_devices_card_off_title);
234         final CharSequence summary = mContext.getText(R.string.bluetooth_devices_card_off_summary);
235         final Intent intent = new Intent(getUri().toString())
236                 .setClass(mContext, SliceBroadcastReceiver.class)
237                 .putExtra(EXTRA_ENABLE_BLUETOOTH, true);
238         final SliceAction action = SliceAction.create(PendingIntent.getBroadcast(mContext,
239                 0 /* requestCode */, intent, 0 /* flags */), icon, ListBuilder.ICON_IMAGE, title);
240 
241         return new ListBuilder.RowBuilder()
242                 .setTitleItem(icon, ListBuilder.ICON_IMAGE)
243                 .setTitle(title)
244                 .setSubtitle(summary)
245                 .setPrimaryAction(action);
246     }
247 
getBluetoothOnHeader()248     private ListBuilder.RowBuilder getBluetoothOnHeader() {
249         final Drawable drawable = mContext.getDrawable(
250                 com.android.internal.R.drawable.ic_settings_bluetooth);
251         drawable.setTint(Utils.getColorAccentDefaultColor(mContext));
252         final IconCompat icon = Utils.createIconWithDrawable(drawable);
253         final CharSequence title = mContext.getText(R.string.bluetooth_devices);
254         final PendingIntent primaryActionIntent = PendingIntent.getActivity(mContext,
255                 0 /* requestCode */, getIntent(), 0 /* flags */);
256         final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryActionIntent, icon,
257                 ListBuilder.ICON_IMAGE, title);
258 
259         return new ListBuilder.RowBuilder()
260                 .setTitleItem(icon, ListBuilder.ICON_IMAGE)
261                 .setTitle(title)
262                 .setPrimaryAction(primarySliceAction)
263                 .addEndItem(getPairNewDeviceAction());
264     }
265 
getPairNewDeviceAction()266     private SliceAction getPairNewDeviceAction() {
267         final Drawable drawable = mContext.getDrawable(R.drawable.ic_add_24dp);
268         drawable.setTint(Utils.getColorAccentDefaultColor(mContext));
269         final IconCompat icon = Utils.createIconWithDrawable(drawable);
270         final String title = mContext.getString(R.string.bluetooth_pairing_pref_title);
271         final Intent intent = new SubSettingLauncher(mContext)
272                 .setDestination(BluetoothPairingDetail.class.getName())
273                 .setTitleRes(R.string.bluetooth_pairing_page_title)
274                 .setSourceMetricsCategory(SettingsEnums.BLUETOOTH_PAIRING)
275                 .toIntent();
276         final PendingIntent pi = PendingIntent.getActivity(mContext, intent.hashCode(), intent,
277                 0 /* flags */);
278         return SliceAction.createDeeplink(pi, icon, ListBuilder.ICON_IMAGE, title);
279     }
280 
getBluetoothRowBuilders()281     private List<ListBuilder.RowBuilder> getBluetoothRowBuilders() {
282         final List<ListBuilder.RowBuilder> bluetoothRows = new ArrayList<>();
283         final List<CachedBluetoothDevice> pairedDevices = getPairedBluetoothDevices();
284         if (pairedDevices.isEmpty()) {
285             return bluetoothRows;
286         }
287 
288         // Initialize updaters without being blocked after paired devices is available because
289         // LocalBluetoothManager is ready.
290         lazyInitUpdaters();
291 
292         // Create row builders based on paired devices.
293         for (CachedBluetoothDevice device : pairedDevices) {
294             if (bluetoothRows.size() >= DEFAULT_EXPANDED_ROW_COUNT) {
295                 break;
296             }
297 
298             String summary = device.getConnectionSummary();
299             if (summary == null) {
300                 summary = mContext.getString(
301                         R.string.connected_device_previously_connected_screen_title);
302             }
303             final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder()
304                     .setTitleItem(getBluetoothDeviceIcon(device), ListBuilder.ICON_IMAGE)
305                     .setTitle(device.getName())
306                     .setSubtitle(summary);
307 
308             if (mAvailableMediaBtDeviceUpdater.isFilterMatched(device)
309                     || mSavedBtDeviceUpdater.isFilterMatched(device)) {
310                 // For all available media devices and previously connected devices, the primary
311                 // action is to activate or connect, and the end gear icon links to detail page.
312                 rowBuilder.setPrimaryAction(buildPrimaryBluetoothAction(device));
313                 rowBuilder.addEndItem(buildBluetoothDetailDeepLinkAction(device));
314             } else {
315                 // For other devices, the primary action is to link to detail page.
316                 rowBuilder.setPrimaryAction(buildBluetoothDetailDeepLinkAction(device));
317             }
318 
319             bluetoothRows.add(rowBuilder);
320         }
321 
322         return bluetoothRows;
323     }
324 
lazyInitUpdaters()325     private void lazyInitUpdaters() {
326         if (mAvailableMediaBtDeviceUpdater == null) {
327             mAvailableMediaBtDeviceUpdater = new AvailableMediaBluetoothDeviceUpdater(mContext,
328                     null /* fragment */, null /* devicePreferenceCallback */);
329         }
330 
331         if (mSavedBtDeviceUpdater == null) {
332             mSavedBtDeviceUpdater = new SavedBluetoothDeviceUpdater(mContext,
333                     null /* fragment */, null /* devicePreferenceCallback */);
334         }
335     }
336 
337     @VisibleForTesting
buildPrimaryBluetoothAction(CachedBluetoothDevice bluetoothDevice)338     SliceAction buildPrimaryBluetoothAction(CachedBluetoothDevice bluetoothDevice) {
339         final Intent intent = new Intent(getUri().toString())
340                 .setClass(mContext, SliceBroadcastReceiver.class)
341                 .putExtra(BLUETOOTH_DEVICE_HASH_CODE, bluetoothDevice.hashCode());
342 
343         return SliceAction.create(
344                 PendingIntent.getBroadcast(mContext, bluetoothDevice.hashCode(), intent, 0),
345                 getBluetoothDeviceIcon(bluetoothDevice),
346                 ListBuilder.ICON_IMAGE,
347                 bluetoothDevice.getName());
348     }
349 
350     @VisibleForTesting
buildBluetoothDetailDeepLinkAction(CachedBluetoothDevice bluetoothDevice)351     SliceAction buildBluetoothDetailDeepLinkAction(CachedBluetoothDevice bluetoothDevice) {
352         return SliceAction.createDeeplink(
353                 getBluetoothDetailIntent(bluetoothDevice),
354                 IconCompat.createWithResource(mContext, R.drawable.ic_settings_accent),
355                 ListBuilder.ICON_IMAGE,
356                 bluetoothDevice.getName());
357     }
358 
isBluetoothEnabled(BluetoothAdapter btAdapter)359     private boolean isBluetoothEnabled(BluetoothAdapter btAdapter) {
360         switch (btAdapter.getState()) {
361             case BluetoothAdapter.STATE_ON:
362             case BluetoothAdapter.STATE_TURNING_ON:
363                 return true;
364             default:
365                 return false;
366         }
367     }
368 }
369