1 /*
2  * Copyright (C) 2020 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.tv.settings.accessories;
18 
19 
20 import static android.app.PendingIntent.FLAG_IMMUTABLE;
21 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
22 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
23 
24 import static com.android.tv.settings.accessories.AddAccessoryActivity.ACTION_CONNECT_INPUT;
25 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.ACTION_FIND_MY_REMOTE;
26 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.ACTION_TOGGLE_CHANGED;
27 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.ACTIVE_AUDIO_OUTPUT;
28 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.BLUETOOTH_ON;
29 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.EXTRA_TOGGLE_STATE;
30 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.EXTRA_TOGGLE_TYPE;
31 import static com.android.tv.settings.accessories.ConnectedDevicesSliceUtils.EXTRAS_SLICE_URI;
32 import static com.android.tv.settings.accessories.ConnectedDevicesSliceUtils.FIND_MY_REMOTE_PHYSICAL_BUTTON_ENABLED_SETTING;
33 import static com.android.tv.settings.accessories.ConnectedDevicesSliceUtils.isFindMyRemoteButtonEnabled;
34 
35 import android.app.PendingIntent;
36 import android.app.admin.DevicePolicyManager;
37 import android.app.tvsettings.TvSettingsEnums;
38 import android.bluetooth.BluetoothDevice;
39 import android.content.ComponentName;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.ServiceConnection;
43 import android.content.pm.ResolveInfo;
44 import android.net.Uri;
45 import android.os.Bundle;
46 import android.os.Handler;
47 import android.os.IBinder;
48 import android.os.Looper;
49 import android.os.StrictMode;
50 import android.os.UserHandle;
51 import android.os.UserManager;
52 import android.provider.Settings;
53 import android.text.TextUtils;
54 import android.util.ArrayMap;
55 import android.util.Log;
56 
57 import androidx.annotation.IntegerRes;
58 import androidx.core.graphics.drawable.IconCompat;
59 import androidx.slice.Slice;
60 import androidx.slice.SliceProvider;
61 
62 import com.android.settingslib.RestrictedLockUtils;
63 import com.android.settingslib.RestrictedLockUtilsInternal;
64 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
65 import com.android.settingslib.media.flags.Flags;
66 import com.android.tv.settings.R;
67 import com.android.tv.twopanelsettings.slices.builders.PreferenceSliceBuilder;
68 import com.android.tv.twopanelsettings.slices.builders.PreferenceSliceBuilder.RowBuilder;
69 
70 import java.util.ArrayList;
71 import java.util.Collections;
72 import java.util.HashMap;
73 import java.util.HashSet;
74 import java.util.List;
75 import java.util.Map;
76 import java.util.Set;
77 
78 /** The SliceProvider for "connected devices" settings */
79 public class ConnectedDevicesSliceProvider extends SliceProvider implements
80         BluetoothDeviceProvider.Listener {
81 
82     private static final String TAG = "ConnectedDevices";
83     private static final boolean DEBUG = false;
84     private static final boolean DISCONNECT_PREFERENCE_ENABLED = false;
85     private static final int ACTIVE_AUDIO_OUTPUT_INTENT_REQUEST_CODE = 9;
86     private final Map<Uri, Integer> mPinnedUris = new ArrayMap<>();
87     private final Handler mHandler = new Handler(Looper.getMainLooper());
88 
89     private boolean mBtDeviceServiceBound;
90     private BluetoothDevicesService.LocalBinder mBtDeviceServiceBinder;
91 
92     private final BluetoothDeviceProvider mLocalBluetoothDeviceProvider =
93             new LocalBluetoothDeviceProvider() {
94                 @Override
95                 BluetoothDeviceProvider getHostBluetoothDeviceProvider() {
96                     return getBluetoothDeviceProvider();
97                 }
98     };
99 
100     private final ServiceConnection mBtDeviceServiceConnection =
101             new SimplifiedConnection() {
102 
103                 @Override
104                 public void onServiceConnected(ComponentName className, IBinder service) {
105                     mBtDeviceServiceBinder = (BluetoothDevicesService.LocalBinder) service;
106                     mBtDeviceServiceBinder.addListener(ConnectedDevicesSliceProvider.this);
107                     getContext().getContentResolver()
108                             .notifyChange(ConnectedDevicesSliceUtils.GENERAL_SLICE_URI, null);
109                 }
110 
111                 @Override
112                 protected void cleanUp() {
113                     if (mBtDeviceServiceBinder != null) {
114                         mBtDeviceServiceBinder.removeListener(ConnectedDevicesSliceProvider.this);
115                     }
116                     mBtDeviceServiceBinder = null;
117                 }
118             };
119 
120     static final String KEY_BLUETOOTH_TOGGLE = "bluetooth_toggle";
121     static final String KEY_PAIR_REMOTE = "pair_remote";
122     static final String KEY_ACCESSORIES = "accessories";
123     static final String KEY_OFFICIAL_REMOTES_CATEGORY = "official_remotes_category";
124     // Preference key for the remote bundled with the device (Bluetooth based)
125     static final String KEY_OFFICIAL_REMOTE = "official_remote";
126     // Preference key for the remote bundled with the device (IR based)
127     static final String KEY_IR = "ir";
128     static final String KEY_CONNECT = "connect";
129     static final String KEY_DISCONNECT = "disconnect";
130     static final String KEY_RENAME = "rename";
131     static final String KEY_FORGET = "forget";
132     static final String KEY_EXTRAS_DEVICE = "extra_devices";
133     static final String KEY_BLUETOOTH_DEVICE_INFO = "bluetooth_device_info";
134     static final String KEY_FIND_MY_REMOTE_TOGGLE = "fmr_toggle";
135     static final String KEY_TOGGLE_ACTIVE_AUDIO_OUTPUT = "toggle_active_audio_output";
136 
137     static final int YES = R.string.general_action_yes;
138     static final int NO = R.string.general_action_no;
139     static final int[] YES_NO_ARGS = {YES, NO};
140 
141     @Override
onCreateSliceProvider()142     public boolean onCreateSliceProvider() {
143         return true;
144     }
145 
146     @Override
onCreatePermissionRequest(Uri sliceUri, String callingPackage)147     public PendingIntent onCreatePermissionRequest(Uri sliceUri, String callingPackage) {
148         final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS);
149         final PendingIntent noOpIntent = PendingIntent.getActivity(
150                 getContext(), 0, settingsIntent, PendingIntent.FLAG_IMMUTABLE);
151         return noOpIntent;
152     }
153 
154     @Override
onSlicePinned(Uri sliceUri)155     public void onSlicePinned(Uri sliceUri) {
156         mHandler.post(() -> {
157             if (DEBUG) {
158                 Log.d(TAG, "Slice pinned: " + sliceUri);
159             }
160             Context context = getContext();
161             if (!mBtDeviceServiceBound && context.bindService(
162                     new Intent(context, AccessoryUtils.getBluetoothDeviceServiceClass()),
163                     mBtDeviceServiceConnection,
164                     Context.BIND_AUTO_CREATE)) {
165                 mBtDeviceServiceBound = true;
166             }
167             if (!mPinnedUris.containsKey(sliceUri)) {
168                 mPinnedUris.put(sliceUri, 0);
169             }
170             mPinnedUris.put(sliceUri, mPinnedUris.get(sliceUri) + 1);
171         });
172     }
173 
174     @Override
onBindSlice(Uri sliceUri)175     public Slice onBindSlice(Uri sliceUri) {
176         if (DEBUG) {
177             Log.d(TAG, "onBindSlice: " + sliceUri);
178         }
179         if (getBluetoothDevices().isEmpty()) {
180             sliceUri = ConnectedDevicesSliceUtils.GENERAL_SLICE_URI;
181         }
182         StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
183         try {
184             // Prevent StrictMode from throwing on access to shared preferences.
185             StrictMode.setThreadPolicy(
186                 new StrictMode.ThreadPolicy.Builder(oldPolicy).permitDiskReads().build());
187             if (ConnectedDevicesSliceUtils.isGeneralPath(sliceUri)) {
188                 return createGeneralSlice(sliceUri);
189             } else if (ConnectedDevicesSliceUtils.isBluetoothDevicePath(sliceUri)) {
190                 return createBluetoothDeviceSlice(sliceUri);
191             } else if (ConnectedDevicesSliceUtils.isFindMyRemotePath(sliceUri)) {
192                 return createFindMyRemoteSlice(sliceUri);
193             }
194         } finally {
195             StrictMode.setThreadPolicy(oldPolicy);
196         }
197         return null;
198     }
199 
200     @Override
onSliceUnpinned(Uri sliceUri)201     public void onSliceUnpinned(Uri sliceUri) {
202         mHandler.post(() -> {
203             if (DEBUG) {
204                 Log.d(TAG, "Slice unpinned: " + sliceUri);
205             }
206             Context context = getContext();
207             // If at this point there is only one slice pinned, we need to unbind the service as
208             // there won't be any slice pinned after handleSliceUnpinned is called.
209             if (mPinnedUris.containsKey(sliceUri)) {
210                 int newCount = mPinnedUris.get(sliceUri) - 1;
211                 mPinnedUris.put(sliceUri, newCount);
212                 if (newCount == 0) {
213                     mPinnedUris.remove(sliceUri);
214                 }
215             }
216             if (mPinnedUris.isEmpty() && mBtDeviceServiceBound) {
217                 context.unbindService(mBtDeviceServiceConnection);
218                 mBtDeviceServiceBound = false;
219             }
220         });
221     }
222 
223     // BluetoothDeviceProvider.Listener implementation
224     @Override
onDeviceUpdated(BluetoothDevice device)225     public void onDeviceUpdated(BluetoothDevice device) {
226         getContext().getContentResolver()
227                 .notifyChange(ConnectedDevicesSliceUtils.GENERAL_SLICE_URI, null);
228         notifyDeviceSlice(device);
229     }
230 
231     // The initial slice in the Connected Device flow.
createGeneralSlice(Uri sliceUri)232     private Slice createGeneralSlice(Uri sliceUri) {
233         PreferenceSliceBuilder psb = new PreferenceSliceBuilder(getContext(), sliceUri);
234         psb.addScreenTitle(
235                 new RowBuilder()
236                         .setTitle(getString(R.string.connected_devices_slice_pref_title))
237                         .setPageId(TvSettingsEnums.CONNECTED_SLICE));
238         updateBluetoothToggle(psb);
239         updatePairingButton(psb);
240         updateConnectedDevices(psb);
241         updateOfficialRemoteSettings(psb);
242         updateFmr(psb);
243         return psb.build();
244     }
245 
246     // The slice page that shows detail information of a particular device.
createBluetoothDeviceSlice(Uri sliceUri)247     private Slice createBluetoothDeviceSlice(Uri sliceUri) {
248         Context context = getContext();
249         String deviceAddr = ConnectedDevicesSliceUtils.getDeviceAddr(sliceUri);
250         BluetoothDevice device = BluetoothDevicesService.findDevice(deviceAddr);
251         CachedBluetoothDevice cachedDevice =
252                 AccessoryUtils.getCachedBluetoothDevice(getContext(), device);
253         String deviceName = "";
254         if (device != null) {
255             deviceName = AccessoryUtils.getLocalName(device);
256         }
257 
258         PreferenceSliceBuilder psb = new PreferenceSliceBuilder(getContext(), sliceUri);
259         psb.addScreenTitle(
260                 new RowBuilder()
261                         .setTitle(deviceName)
262                         .setPageId(TvSettingsEnums.CONNECTED_SLICE_DEVICE_ENTRY));
263 
264         Bundle extras;
265         Intent i;
266         // Update "Use for TV audio".
267         // Set as active audio output device only connected devices that have audio capabilities
268         if (Flags.enableTvMediaOutputDialog()
269                 && cachedDevice != null && !cachedDevice.isBusy()
270                 && AccessoryUtils.isConnected(device) && cachedDevice.isConnected()
271                 && (AccessoryUtils.isBluetoothHeadset(device)
272                 || AccessoryUtils.hasAudioProfile(cachedDevice))) {
273             boolean isActive = AccessoryUtils.isActiveAudioOutput(device);
274 
275             Intent intent = new Intent(ACTION_TOGGLE_CHANGED);
276             intent.setClass(context, ConnectedDevicesSliceBroadcastReceiver.class);
277             intent.putExtra(EXTRA_TOGGLE_TYPE, ACTIVE_AUDIO_OUTPUT);
278             intent.putExtra(EXTRA_TOGGLE_STATE, !isActive);
279             intent.putExtra(KEY_EXTRAS_DEVICE, device);
280 
281             PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
282                     ACTIVE_AUDIO_OUTPUT_INTENT_REQUEST_CODE, intent,
283                     PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
284 
285             // Update set/unset active audio output preference
286             RowBuilder activeAudioOutputPref = new RowBuilder()
287                     .setKey(KEY_TOGGLE_ACTIVE_AUDIO_OUTPUT)
288                     .setTitle(getString(R.string.bluetooth_toggle_active_audio_output_title))
289                     .setActionId(
290                             TvSettingsEnums.CONNECTED_SLICE_DEVICE_ENTRY_TOGGLE_ACTIVE_AUDIO_OUTPUT)
291                     .addSwitch(pendingIntent,
292                             context.getText(R.string.bluetooth_toggle_active_audio_output_title),
293                             isActive);
294 
295             psb.addPreference(activeAudioOutputPref);
296         }
297 
298         // Update "connect/disconnect preference"
299         if (cachedDevice != null && !cachedDevice.isBusy()) {
300             // Whether the device is actually connected from CachedBluetoothDevice's perspective.
301             boolean isConnected = AccessoryUtils.isConnected(device) && cachedDevice.isConnected();
302 
303             if (!isConnected || showDisconnectButton(device, context)) {
304                 RowBuilder connectionActionPref = new RowBuilder()
305                         .setKey(isConnected ? KEY_DISCONNECT : KEY_CONNECT)
306                         .setTitle(getString((isConnected
307                                 ? R.string.bluetooth_disconnect_action_title
308                                 : R.string.bluetooth_connect_action_title)));
309                 extras = new Bundle();
310                 i = new Intent(context, BluetoothActionActivity.class);
311                 BluetoothActionFragment.prepareArgs(
312                         extras,
313                         isConnected ? KEY_DISCONNECT : KEY_CONNECT,
314                         R.drawable.ic_baseline_bluetooth_searching_large,
315                         isConnected
316                                 ? R.string.bluetooth_disconnect_confirm
317                                 : R.string.bluetooth_connect_confirm,
318                         0,
319                         YES_NO_ARGS,
320                         deviceName,
321                         isConnected ? 1 /* default to NO (index 1) */ : 0 /* default to YES */
322                 );
323                 i.putExtras(extras);
324                 i.putExtra(KEY_EXTRAS_DEVICE, device);
325                 PendingIntent pendingIntent = PendingIntent.getActivity(
326                         context, 3, i,
327                         PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
328                 Intent followUpIntent =
329                         new Intent(context, ConnectedDevicesSliceBroadcastReceiver.class);
330                 followUpIntent.putExtra(EXTRAS_SLICE_URI, sliceUri.toString());
331                 PendingIntent followupIntent = PendingIntent.getBroadcast(
332                         context, 4, followUpIntent,
333                         PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
334                 connectionActionPref.setPendingIntent(pendingIntent);
335                 connectionActionPref.setFollowupPendingIntent(followupIntent);
336                 psb.addPreference(connectionActionPref);
337             }
338         }
339 
340         // Update "rename preference".
341         RowBuilder renamePref = new RowBuilder()
342                 .setKey(KEY_RENAME)
343                 .setTitle(getString(R.string.bluetooth_rename_action_title))
344                 .setActionId(TvSettingsEnums.CONNECTED_SLICE_DEVICE_ENTRY_RENAME);
345         extras = new Bundle();
346         BluetoothActionFragment.prepareArgs(
347                 extras,
348                 KEY_RENAME,
349                 R.drawable.ic_baseline_bluetooth_searching_large,
350                 R.string.bluetooth_rename,
351                 0,
352                 null,
353                 deviceName,
354                 BluetoothActionFragment.DEFAULT_CHOICE_UNDEFINED
355         );
356         i = new Intent(context, BluetoothActionActivity.class);
357         i.putExtra(KEY_EXTRAS_DEVICE, device);
358         i.putExtras(extras);
359         PendingIntent renamePendingIntent = PendingIntent.getActivity(
360                 context, 5, i, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
361 
362         Intent followUpIntent = new Intent(context, ConnectedDevicesSliceBroadcastReceiver.class);
363         followUpIntent.putExtra(EXTRAS_SLICE_URI, sliceUri.toString());
364         PendingIntent renameFollowupIntent = PendingIntent.getBroadcast(
365                 context, 6, followUpIntent,
366                 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
367         renamePref.setFollowupPendingIntent(renameFollowupIntent);
368         renamePref.setPendingIntent(renamePendingIntent);
369         psb.addPreference(renamePref);
370 
371         // Update "forget preference".
372         RowBuilder forgetPref = new RowBuilder()
373                 .setKey(KEY_FORGET)
374                 .setTitle(getString(R.string.bluetooth_forget_action_title))
375                 .setActionId(TvSettingsEnums.CONNECTED_SLICE_DEVICE_ENTRY_FORGET);
376         extras = new Bundle();
377         i = new Intent(context, BluetoothActionActivity.class);
378         BluetoothActionFragment.prepareArgs(
379                 extras,
380                 KEY_FORGET,
381                 R.drawable.ic_baseline_bluetooth_searching_large,
382                 R.string.bluetooth_forget_confirm,
383                 0,
384                 YES_NO_ARGS,
385                 deviceName,
386                 1 /* default to NO (index 1) */
387         );
388         i.putExtras(extras);
389         i.putExtra(KEY_EXTRAS_DEVICE, device);
390         PendingIntent disconnectPendingIntent = PendingIntent.getActivity(
391                 context, 7, i, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
392         followUpIntent = new Intent(context, ConnectedDevicesSliceBroadcastReceiver.class);
393         followUpIntent.putExtra(EXTRAS_SLICE_URI, sliceUri.toString());
394         PendingIntent forgetFollowupIntent = PendingIntent.getBroadcast(
395                 context, 8, followUpIntent,
396                 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
397         forgetPref.setPendingIntent(disconnectPendingIntent);
398         forgetPref.setFollowupPendingIntent(forgetFollowupIntent);
399         psb.addPreference(forgetPref);
400 
401         // Update "bluetooth device info preference".
402         RowBuilder infoPref = new RowBuilder()
403                 .setKey(KEY_BLUETOOTH_DEVICE_INFO)
404                 .setIcon(IconCompat.createWithResource(context, R.drawable.ic_baseline_info_24dp));
405 
406         infoPref.addInfoItem(getString(R.string.bluetooth_serial_number_label), deviceAddr);
407         psb.addPreference(infoPref);
408         return psb.build();
409     }
410 
updateBluetoothToggle(PreferenceSliceBuilder psb)411     private void updateBluetoothToggle(PreferenceSliceBuilder psb) {
412         if (showBluetoothToggle()) {
413             Intent bluetoothToggleIntent;
414             if (AccessoryUtils.isBluetoothEnabled()) {
415                 bluetoothToggleIntent = new Intent(getContext(), BluetoothActionActivity.class);
416                 Bundle extras = new Bundle();
417                 BluetoothActionFragment.prepareArgs(
418                         extras,
419                         KEY_BLUETOOTH_TOGGLE,
420                         R.drawable.ic_baseline_bluetooth_searching_large,
421                         R.string.bluetooth_toggle_confirmation_dialog_title,
422                         R.string.bluetooth_toggle_confirmation_dialog_summary,
423                         YES_NO_ARGS,
424                         null,
425                         0 /* default to YES */
426                 );
427                 bluetoothToggleIntent.putExtras(extras);
428             } else {
429                 bluetoothToggleIntent = new Intent(ACTION_TOGGLE_CHANGED);
430                 bluetoothToggleIntent.setClass(
431                         getContext(), ConnectedDevicesSliceBroadcastReceiver.class);
432                 bluetoothToggleIntent.putExtra(EXTRA_TOGGLE_TYPE, BLUETOOTH_ON);
433             }
434             psb.addPreference(
435                     new RowBuilder()
436                             .setKey(KEY_BLUETOOTH_TOGGLE)
437                             .setIcon(IconCompat.createWithResource(
438                                     getContext(), R.drawable.ic_bluetooth_raw))
439                             .setIconNeedsToBeProcessed(true)
440                             .setTitle(getString(R.string.bluetooth_toggle_title))
441                             .addSwitch(
442                                     AccessoryUtils.isBluetoothEnabled()
443                                             ? PendingIntent.getActivity(
444                                                     getContext(), 1, bluetoothToggleIntent,
445                                                     PendingIntent.FLAG_IMMUTABLE)
446                                             : PendingIntent.getBroadcast(
447                                                     getContext(), 2, bluetoothToggleIntent,
448                                                     PendingIntent.FLAG_IMMUTABLE),
449                                     AccessoryUtils.isBluetoothEnabled())
450             );
451         }
452     }
453 
updatePairingButton(PreferenceSliceBuilder psb)454     private void updatePairingButton(PreferenceSliceBuilder psb) {
455         RestrictedLockUtils.EnforcedAdmin admin =
456                 RestrictedLockUtilsInternal.checkIfRestrictionEnforced(getContext(),
457                         UserManager.DISALLOW_CONFIG_BLUETOOTH, UserHandle.myUserId());
458         if (AccessoryUtils.isBluetoothEnabled()) {
459             PendingIntent pendingIntent;
460             if (admin == null) {
461                 Intent i = new Intent(ACTION_CONNECT_INPUT).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
462                 pendingIntent = PendingIntent
463                         .getActivity(getContext(), 3, i, PendingIntent.FLAG_IMMUTABLE);
464             } else {
465                 Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(getContext(),
466                         admin);
467                 intent.putExtra(DevicePolicyManager.EXTRA_RESTRICTION,
468                         UserManager.DISALLOW_CONFIG_BLUETOOTH);
469                 pendingIntent = PendingIntent.getActivity(getContext(), 0, intent,
470                         PendingIntent.FLAG_IMMUTABLE);
471             }
472             psb.addPreference(new RowBuilder()
473                     .setKey(KEY_PAIR_REMOTE)
474                     .setTitle(getString(R.string.bluetooth_pair_accessory))
475                     .setActionId(TvSettingsEnums.CONNECTED_SLICE_CONNECT_NEW_DEVICES)
476                     .setIcon(IconCompat.createWithResource(getContext(),
477                             R.drawable.ic_baseline_add_24dp))
478                     .setIconNeedsToBeProcessed(true)
479                     .setPendingIntent(pendingIntent)
480             );
481         }
482     }
483 
updateConnectedDevices(PreferenceSliceBuilder psb)484     private void updateConnectedDevices(PreferenceSliceBuilder psb) {
485         // Overall BT devices maps
486         HashMap<String, BluetoothDevice> addressToDevice = new HashMap<>();
487         // Sets for BT devices that are not official remotes:
488         // - activeAccessories: they are considered connected from both BluetoothDevice and
489         //       CachedBluetoothDevice's perceptive.
490         // - inactiveAccessories: they are considered connected from BluetoothDevice's perceptive
491         //       but disconnected from CachedBluetoothDevice's perceptive. They can be easily
492         //       reconnected.
493         // - bondedAccessories: they are considered merely bonded but not connected from
494         //       BluetoothDevice's perceptive.
495         Set<String> activeAccessories = new HashSet<>();
496         Set<String> inactiveAccessories = new HashSet<>();
497         Set<String> bondedAccessories = new HashSet<>();
498 
499         // Bucketing all BT devices
500         for (BluetoothDevice device : getBluetoothDevices()) {
501             CachedBluetoothDevice cachedDevice =
502                     AccessoryUtils.getCachedBluetoothDevice(getContext(), device);
503             if (!AccessoryUtils.isKnownDevice(getContext(), device)) {
504                 if (AccessoryUtils.isConnected(device)) {
505                     addressToDevice.put(device.getAddress(), device);
506                     if (cachedDevice != null && cachedDevice.isConnected()) {
507                         activeAccessories.add(device.getAddress());
508                     } else {
509                         inactiveAccessories.add(device.getAddress());
510                     }
511                 } else if (AccessoryUtils.isBonded(device)) {
512                     addressToDevice.put(device.getAddress(), device);
513                     bondedAccessories.add(device.getAddress());
514                 }
515             }
516         }
517 
518         // "Accessories" category
519         if (activeAccessories.size() + inactiveAccessories.size() + bondedAccessories.size()
520                 > 0) {
521             psb.addPreferenceCategory(new RowBuilder()
522                     .setTitle(getContext().getString(R.string.bluetooth_known_devices_category))
523                     .setKey(KEY_ACCESSORIES));
524             // Add accessories following the ranking of: active, inactive, bonded.
525             createAndAddBtDeviceSlicePreferenceFromSet(psb, activeAccessories, addressToDevice);
526             createAndAddBtDeviceSlicePreferenceFromSet(psb, inactiveAccessories, addressToDevice);
527             createAndAddBtDeviceSlicePreferenceFromSet(psb, bondedAccessories, addressToDevice);
528         }
529     }
530 
updateOfficialRemoteSettings(PreferenceSliceBuilder psb)531     private void updateOfficialRemoteSettings(PreferenceSliceBuilder psb) {
532         String officialRemoteSettingsUri =
533                 getString(R.string.bluetooth_official_remote_entry_slice_uri);
534         String irSettingsUri =
535                 getString(R.string.bluetooth_ir_entry_slice_uri);
536         boolean isOfficialRemoteSettingsUriValid = isSliceProviderValid(officialRemoteSettingsUri);
537         boolean isIrSettingsUriValid = isSliceProviderValid(irSettingsUri);
538         if (isOfficialRemoteSettingsUriValid || isIrSettingsUriValid) {
539             psb.addPreferenceCategory(new RowBuilder()
540                     .setKey(KEY_OFFICIAL_REMOTES_CATEGORY)
541                     .setTitle(getString(R.string.bluetooth_official_remote_category)));
542         }
543         if (isIrSettingsUriValid) {
544             psb.addPreference(new RowBuilder()
545                     .setKey(KEY_IR)
546                     .setTitle(getString(R.string.bluetooth_ir_entry_title))
547                     .setSubtitle(getString(R.string.bluetooth_ir_entry_subtitle))
548                     .setTargetSliceUri(irSettingsUri));
549         }
550         if (isOfficialRemoteSettingsUriValid) {
551             psb.addPreference(new RowBuilder()
552                     .setKey(KEY_OFFICIAL_REMOTE)
553                     .setTitle(getString(R.string.bluetooth_official_remote_entry_title))
554                     .setTargetSliceUri(officialRemoteSettingsUri));
555         }
556     }
557 
updateFmr(PreferenceSliceBuilder psb)558     private void updateFmr(PreferenceSliceBuilder psb) {
559         List<ResolveInfo> receivers = getContext().getPackageManager().queryBroadcastReceivers(
560                 new Intent(ACTION_FIND_MY_REMOTE), 0);
561         if (receivers.isEmpty()) {
562             return;
563         }
564 
565         psb.addPreference(new RowBuilder()
566                 .setKey(KEY_FIND_MY_REMOTE_TOGGLE)
567                 .setTitle(getString(R.string.settings_find_my_remote_title))
568                 .setSubtitle(getString(R.string.settings_find_my_remote_description))
569                 .setTargetSliceUri(ConnectedDevicesSliceUtils.FIND_MY_REMOTE_SLICE_URI.toString()));
570     }
571 
createAndAddBtDeviceSlicePreferenceFromSet( PreferenceSliceBuilder psb, Set<String> addresses, HashMap<String, BluetoothDevice> addressesToBtDeviceMap)572     private void createAndAddBtDeviceSlicePreferenceFromSet(
573             PreferenceSliceBuilder psb,
574             Set<String> addresses,
575             HashMap<String, BluetoothDevice> addressesToBtDeviceMap) {
576         if (psb == null || addresses == null || addresses.isEmpty()
577                 || addressesToBtDeviceMap == null || addressesToBtDeviceMap.isEmpty()) {
578             return;
579         }
580         final List<String> devicesAddressesList = new ArrayList<>(addresses);
581         Collections.sort(devicesAddressesList);
582         for (String deviceAddr : devicesAddressesList) {
583             psb.addPreference(
584                     createBtDeviceSlicePreference(
585                             getContext(),
586                             addressesToBtDeviceMap.get(deviceAddr)));
587         }
588     }
589 
createBtDeviceSlicePreference( Context context, BluetoothDevice device)590     private PreferenceSliceBuilder.RowBuilder createBtDeviceSlicePreference(
591             Context context, BluetoothDevice device) {
592         PreferenceSliceBuilder.RowBuilder pref = new PreferenceSliceBuilder.RowBuilder();
593         boolean isConnected = AccessoryUtils.isConnected(device)
594                 && AccessoryUtils.getCachedBluetoothDevice(getContext(), device) != null
595                 && AccessoryUtils.getCachedBluetoothDevice(getContext(), device).isConnected();
596         pref.setKey(device.getAddress());
597         pref.setTitle(AccessoryUtils.getLocalName(device));
598         pref.setSubtitle(
599                 isConnected
600                         ? getString(R.string.bluetooth_connected_status)
601                         : getString(R.string.bluetooth_disconnected_status));
602         pref.setIcon(IconCompat.createWithResource(
603                 context, AccessoriesFragment.getImageIdForDevice(device, true)));
604         pref.setIconNeedsToBeProcessed(true);
605 
606         RestrictedLockUtils.EnforcedAdmin admin =
607                 RestrictedLockUtilsInternal.checkIfRestrictionEnforced(getContext(),
608                         UserManager.DISALLOW_CONFIG_BLUETOOTH, UserHandle.myUserId());
609         if (admin == null) {
610             Uri targetSliceUri = ConnectedDevicesSliceUtils
611                     .getDeviceUri(device.getAddress(), device.getAlias());
612             pref.setTargetSliceUri(targetSliceUri.toString());
613         } else {
614             Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(getContext(),
615                     admin);
616             intent.putExtra(DevicePolicyManager.EXTRA_RESTRICTION,
617                     UserManager.DISALLOW_CONFIG_BLUETOOTH);
618             pref.setPendingIntent(PendingIntent.getActivity(getContext(), 0, intent,
619                     PendingIntent.FLAG_IMMUTABLE));
620         }
621         return pref;
622     }
623 
getBluetoothDevices()624     private List<BluetoothDevice> getBluetoothDevices() {
625         if (mBtDeviceServiceBinder != null) {
626             return mBtDeviceServiceBinder.getDevices();
627         }
628         return new ArrayList<>();
629     }
630 
getBluetoothDeviceProvider()631     private BluetoothDeviceProvider getBluetoothDeviceProvider() {
632         return mBtDeviceServiceBinder;
633     }
634 
notifyDeviceSlice(BluetoothDevice device)635     private void notifyDeviceSlice(BluetoothDevice device) {
636         String addr = device.getAddress();
637         mHandler.post(() -> {
638             if (device != null) {
639                 getContext().getContentResolver().notifyChange(
640                         ConnectedDevicesSliceUtils.getDeviceUri(addr, device.getAlias()), null);
641             }
642         });
643     }
644 
showBluetoothToggle()645     private boolean showBluetoothToggle() {
646         return getContext().getResources().getBoolean(R.bool.show_bluetooth_toggle);
647     }
648 
getString(@ntegerRes int resId)649     private String getString(@IntegerRes int resId) {
650         return getContext().getString(resId);
651     }
652 
isSliceProviderValid(String uri)653     private boolean isSliceProviderValid(String uri) {
654         return !TextUtils.isEmpty(uri)
655                 && ConnectedDevicesSliceUtils.isSliceProviderValid(getContext(), uri);
656     }
657 
showDisconnectButton(BluetoothDevice device, Context context)658     private boolean showDisconnectButton(BluetoothDevice device, Context context) {
659         if (DISCONNECT_PREFERENCE_ENABLED) {
660             return true;
661         }
662         return !AccessoryUtils.isRemoteClass(device)
663                 && !AccessoryUtils.isKnownDevice(context, device);
664     }
665 
createFindMyRemoteSlice(Uri sliceUri)666     private Slice createFindMyRemoteSlice(Uri sliceUri) {
667         Context context = getContext();
668         final PreferenceSliceBuilder psb = new PreferenceSliceBuilder(context, sliceUri);
669         psb.addScreenTitle(new RowBuilder()
670                 .setTitle(getString(R.string.settings_find_my_remote_title))
671                 .setSubtitle(getString(R.string.find_my_remote_slice_description)));
672 
673         if (context.getResources().getBoolean(R.bool.config_find_my_remote_integration_enabled)) {
674             boolean isButtonEnabled = isFindMyRemoteButtonEnabled(context);
675             Intent intent = new Intent(ACTION_TOGGLE_CHANGED);
676             intent.putExtra(EXTRA_TOGGLE_TYPE, FIND_MY_REMOTE_PHYSICAL_BUTTON_ENABLED_SETTING);
677             intent.putExtra(EXTRA_TOGGLE_STATE, !isButtonEnabled);
678             intent.setClass(context, ConnectedDevicesSliceBroadcastReceiver.class);
679             psb.addPreference(new RowBuilder()
680                     .setKey(FIND_MY_REMOTE_PHYSICAL_BUTTON_ENABLED_SETTING)
681                     .setTitle(getString(R.string.find_my_remote_integration_title))
682                     .setSubtitle(getString(R.string.find_my_remote_integration_hint))
683                     .addSwitch(
684                             PendingIntent.getBroadcast(
685                                     context, 0, intent, FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT),
686                             !isButtonEnabled));
687         }
688 
689         PendingIntent pendingIntent = PendingIntent.getBroadcast(
690                 context, 0,
691                 new Intent(context, ConnectedDevicesSliceBroadcastReceiver.class)
692                         .setAction(ACTION_FIND_MY_REMOTE)
693                         .setFlags(FLAG_RECEIVER_FOREGROUND),
694                 FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT);
695 
696         psb.addPreference(new RowBuilder()
697                 .setKey(ACTION_FIND_MY_REMOTE)
698                 .setTitle(getString(R.string.find_my_remote_play_sound))
699                 .setPendingIntent(pendingIntent)
700                 .setIcon(IconCompat.createWithResource(context, R.drawable.ic_play_arrow))
701                 .setIconNeedsToBeProcessed(true));
702         return psb.build();
703     }
704 }
705