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