• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.bluetooth;
18 
19 import static android.bluetooth.BluetoothDevice.BOND_NONE;
20 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
21 
22 import android.app.settings.SettingsEnums;
23 import android.bluetooth.BluetoothDevice;
24 import android.content.Context;
25 import android.content.res.TypedArray;
26 import android.hardware.input.InputManager;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.os.UserManager;
30 import android.provider.DeviceConfig;
31 import android.text.TextUtils;
32 import android.util.FeatureFlagUtils;
33 import android.util.Log;
34 import android.view.InputDevice;
35 import android.view.LayoutInflater;
36 import android.view.Menu;
37 import android.view.MenuInflater;
38 import android.view.MenuItem;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.ViewTreeObserver;
42 
43 import androidx.annotation.Nullable;
44 import androidx.annotation.VisibleForTesting;
45 
46 import com.android.settings.R;
47 import com.android.settings.connecteddevice.stylus.StylusDevicesController;
48 import com.android.settings.core.SettingsUIDeviceConfig;
49 import com.android.settings.dashboard.RestrictedDashboardFragment;
50 import com.android.settings.inputmethod.KeyboardSettingsPreferenceController;
51 import com.android.settings.overlay.FeatureFactory;
52 import com.android.settings.slices.SlicePreferenceController;
53 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
54 import com.android.settingslib.bluetooth.LocalBluetoothManager;
55 import com.android.settingslib.core.AbstractPreferenceController;
56 import com.android.settingslib.core.lifecycle.Lifecycle;
57 
58 import java.util.ArrayList;
59 import java.util.List;
60 
61 public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment {
62     public static final String KEY_DEVICE_ADDRESS = "device_address";
63     private static final String TAG = "BTDeviceDetailsFrg";
64 
65     @VisibleForTesting
66     static int EDIT_DEVICE_NAME_ITEM_ID = Menu.FIRST;
67 
68     /**
69      * An interface to let tests override the normal mechanism for looking up the
70      * CachedBluetoothDevice and LocalBluetoothManager, and substitute their own mocks instead.
71      * This is only needed in situations where you instantiate the fragment indirectly (eg via an
72      * intent) and can't use something like spying on an instance you construct directly via
73      * newInstance.
74      */
75     @VisibleForTesting
76     interface TestDataFactory {
getDevice(String deviceAddress)77         CachedBluetoothDevice getDevice(String deviceAddress);
78 
getManager(Context context)79         LocalBluetoothManager getManager(Context context);
80 
getUserManager()81         UserManager getUserManager();
82     }
83 
84     @VisibleForTesting
85     static TestDataFactory sTestDataFactory;
86 
87     @VisibleForTesting
88     String mDeviceAddress;
89     @VisibleForTesting
90     LocalBluetoothManager mManager;
91     @VisibleForTesting
92     CachedBluetoothDevice mCachedDevice;
93 
94     @Nullable
95     InputDevice mInputDevice;
96 
97     private UserManager mUserManager;
98 
BluetoothDeviceDetailsFragment()99     public BluetoothDeviceDetailsFragment() {
100         super(DISALLOW_CONFIG_BLUETOOTH);
101     }
102 
103     @VisibleForTesting
getLocalBluetoothManager(Context context)104     LocalBluetoothManager getLocalBluetoothManager(Context context) {
105         if (sTestDataFactory != null) {
106             return sTestDataFactory.getManager(context);
107         }
108         return Utils.getLocalBtManager(context);
109     }
110 
111     @VisibleForTesting
getCachedDevice(String deviceAddress)112     CachedBluetoothDevice getCachedDevice(String deviceAddress) {
113         if (sTestDataFactory != null) {
114             return sTestDataFactory.getDevice(deviceAddress);
115         }
116         BluetoothDevice remoteDevice =
117                 mManager.getBluetoothAdapter().getRemoteDevice(deviceAddress);
118         return mManager.getCachedDeviceManager().findDevice(remoteDevice);
119     }
120 
121     @VisibleForTesting
getUserManager()122     UserManager getUserManager() {
123         if (sTestDataFactory != null) {
124             return sTestDataFactory.getUserManager();
125         }
126 
127         return getSystemService(UserManager.class);
128     }
129 
130     @Nullable
131     @VisibleForTesting
getInputDevice(Context context)132     InputDevice getInputDevice(Context context) {
133         InputManager im = context.getSystemService(InputManager.class);
134 
135         for (int deviceId : im.getInputDeviceIds()) {
136             String btAddress = im.getInputDeviceBluetoothAddress(deviceId);
137 
138             if (btAddress != null && btAddress.equals(mDeviceAddress)) {
139                 return im.getInputDevice(deviceId);
140             }
141         }
142         return null;
143     }
144 
newInstance(String deviceAddress)145     public static BluetoothDeviceDetailsFragment newInstance(String deviceAddress) {
146         Bundle args = new Bundle(1);
147         args.putString(KEY_DEVICE_ADDRESS, deviceAddress);
148         BluetoothDeviceDetailsFragment fragment = new BluetoothDeviceDetailsFragment();
149         fragment.setArguments(args);
150         return fragment;
151     }
152 
153     @Override
onAttach(Context context)154     public void onAttach(Context context) {
155         mDeviceAddress = getArguments().getString(KEY_DEVICE_ADDRESS);
156         mManager = getLocalBluetoothManager(context);
157         mCachedDevice = getCachedDevice(mDeviceAddress);
158         mUserManager = getUserManager();
159 
160         if (FeatureFlagUtils.isEnabled(context,
161                 FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES)) {
162             mInputDevice = getInputDevice(context);
163         }
164 
165         super.onAttach(context);
166         if (mCachedDevice == null) {
167             // Close this page if device is null with invalid device mac address
168             Log.w(TAG, "onAttach() CachedDevice is null!");
169             finish();
170             return;
171         }
172         use(AdvancedBluetoothDetailsHeaderController.class).init(mCachedDevice);
173         use(LeAudioBluetoothDetailsHeaderController.class).init(mCachedDevice, mManager);
174         use(KeyboardSettingsPreferenceController.class).init(mCachedDevice);
175 
176         final BluetoothFeatureProvider featureProvider = FeatureFactory.getFactory(
177                 context).getBluetoothFeatureProvider();
178         final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
179                 SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
180 
181         use(BlockingPrefWithSliceController.class).setSliceUri(sliceEnabled
182                 ? featureProvider.getBluetoothDeviceSettingsUri(mCachedDevice.getDevice())
183                 : null);
184     }
185 
updateExtraControlUri(int viewWidth)186     private void updateExtraControlUri(int viewWidth) {
187         BluetoothFeatureProvider featureProvider = FeatureFactory.getFactory(
188                 getContext()).getBluetoothFeatureProvider();
189         boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
190                 SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
191         Uri controlUri = null;
192         String uri = featureProvider.getBluetoothDeviceControlUri(mCachedDevice.getDevice());
193         if (!TextUtils.isEmpty(uri)) {
194             try {
195                 controlUri = Uri.parse(uri + viewWidth);
196             } catch (NullPointerException exception) {
197                 Log.d(TAG, "unable to parse uri");
198                 controlUri = null;
199             }
200         }
201         final SlicePreferenceController slicePreferenceController = use(
202                 SlicePreferenceController.class);
203         slicePreferenceController.setSliceUri(sliceEnabled ? controlUri : null);
204         slicePreferenceController.onStart();
205         slicePreferenceController.displayPreference(getPreferenceScreen());
206     }
207 
208     private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener =
209             new ViewTreeObserver.OnGlobalLayoutListener() {
210                 @Override
211                 public void onGlobalLayout() {
212                     View view = getView();
213                     if (view == null) {
214                         return;
215                     }
216                     if (view.getWidth() <= 0) {
217                         return;
218                     }
219                     updateExtraControlUri(view.getWidth() - getPaddingSize());
220                     view.getViewTreeObserver().removeOnGlobalLayoutListener(
221                             mOnGlobalLayoutListener);
222                 }
223             };
224 
225     @Override
onCreate(Bundle savedInstanceState)226     public void onCreate(Bundle savedInstanceState) {
227         super.onCreate(savedInstanceState);
228         setTitleForInputDevice();
229     }
230 
231     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)232     public View onCreateView(LayoutInflater inflater, ViewGroup container,
233             Bundle savedInstanceState) {
234         View view = super.onCreateView(inflater, container, savedInstanceState);
235         if (view != null) {
236             view.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
237         }
238         return view;
239     }
240 
241     @Override
onResume()242     public void onResume() {
243         super.onResume();
244         finishFragmentIfNecessary();
245     }
246 
247     @VisibleForTesting
finishFragmentIfNecessary()248     void finishFragmentIfNecessary() {
249         if (mCachedDevice.getBondState() == BOND_NONE) {
250             finish();
251             return;
252         }
253     }
254 
255     @Override
getMetricsCategory()256     public int getMetricsCategory() {
257         return SettingsEnums.BLUETOOTH_DEVICE_DETAILS;
258     }
259 
260     @Override
getLogTag()261     protected String getLogTag() {
262         return TAG;
263     }
264 
265     @Override
getPreferenceScreenResId()266     protected int getPreferenceScreenResId() {
267         return R.xml.bluetooth_device_details_fragment;
268     }
269 
270     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)271     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
272         if (!mUserManager.isGuestUser()) {
273             MenuItem item = menu.add(0, EDIT_DEVICE_NAME_ITEM_ID, 0,
274                     R.string.bluetooth_rename_button);
275             item.setIcon(com.android.internal.R.drawable.ic_mode_edit);
276             item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
277         }
278         super.onCreateOptionsMenu(menu, inflater);
279     }
280 
281     @Override
onOptionsItemSelected(MenuItem menuItem)282     public boolean onOptionsItemSelected(MenuItem menuItem) {
283         if (menuItem.getItemId() == EDIT_DEVICE_NAME_ITEM_ID) {
284             RemoteDeviceNameDialogFragment.newInstance(mCachedDevice).show(
285                     getFragmentManager(), RemoteDeviceNameDialogFragment.TAG);
286             return true;
287         }
288         return super.onOptionsItemSelected(menuItem);
289     }
290 
291     @Override
createPreferenceControllers(Context context)292     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
293         ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
294 
295         if (mCachedDevice != null) {
296             Lifecycle lifecycle = getSettingsLifecycle();
297             controllers.add(new BluetoothDetailsHeaderController(context, this, mCachedDevice,
298                     lifecycle, mManager));
299             controllers.add(new BluetoothDetailsButtonsController(context, this, mCachedDevice,
300                     lifecycle));
301             controllers.add(new BluetoothDetailsCompanionAppsController(context, this,
302                     mCachedDevice, lifecycle));
303             controllers.add(new BluetoothDetailsAudioDeviceTypeController(context, this, mManager,
304                     mCachedDevice, lifecycle));
305             controllers.add(new BluetoothDetailsSpatialAudioController(context, this, mCachedDevice,
306                     lifecycle));
307             controllers.add(new BluetoothDetailsProfilesController(context, this, mManager,
308                     mCachedDevice, lifecycle));
309             controllers.add(new BluetoothDetailsMacAddressController(context, this, mCachedDevice,
310                     lifecycle));
311             controllers.add(new StylusDevicesController(context, mInputDevice, mCachedDevice,
312                     lifecycle));
313             controllers.add(new BluetoothDetailsRelatedToolsController(context, this, mCachedDevice,
314                     lifecycle));
315             controllers.add(new BluetoothDetailsPairOtherController(context, this, mCachedDevice,
316                     lifecycle));
317             controllers.add(new BluetoothDetailsHearingDeviceControlsController(context, this,
318                     mCachedDevice, lifecycle));
319             controllers.add(new BluetoothDetailsDataSyncController(context, this,
320                     mCachedDevice, lifecycle));
321         }
322         return controllers;
323     }
324 
getPaddingSize()325     private int getPaddingSize() {
326         TypedArray resolvedAttributes =
327                 getContext().obtainStyledAttributes(
328                         new int[]{
329                                 android.R.attr.listPreferredItemPaddingStart,
330                                 android.R.attr.listPreferredItemPaddingEnd
331                         });
332         int width = resolvedAttributes.getDimensionPixelSize(0, 0)
333                 + resolvedAttributes.getDimensionPixelSize(1, 0);
334         resolvedAttributes.recycle();
335         return width;
336     }
337 
338     @VisibleForTesting
setTitleForInputDevice()339     void setTitleForInputDevice() {
340         if (StylusDevicesController.isDeviceStylus(mInputDevice, mCachedDevice)) {
341             // This will override the default R.string.device_details_title "Device Details"
342             // that will show on non-stylus bluetooth devices.
343             // That title is set via the manifest and also from BluetoothDeviceUpdater.
344             getActivity().setTitle(getContext().getString(R.string.stylus_device_details_title));
345         }
346     }
347 }
348