1 /* 2 * Copyright (C) 2008 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.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; 20 21 import android.app.settings.SettingsEnums; 22 import android.bluetooth.BluetoothDevice; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.res.Resources; 26 import android.graphics.drawable.Drawable; 27 import android.os.UserManager; 28 import android.text.Html; 29 import android.text.TextUtils; 30 import android.util.Pair; 31 import android.util.TypedValue; 32 import android.view.View; 33 import android.widget.ImageView; 34 35 import androidx.annotation.VisibleForTesting; 36 import androidx.appcompat.app.AlertDialog; 37 import androidx.preference.Preference; 38 import androidx.preference.PreferenceViewHolder; 39 40 import com.android.settings.R; 41 import com.android.settings.overlay.FeatureFactory; 42 import com.android.settings.widget.GearPreference; 43 import com.android.settingslib.bluetooth.BluetoothUtils; 44 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 45 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 46 47 /** 48 * BluetoothDevicePreference is the preference type used to display each remote 49 * Bluetooth device in the Bluetooth Settings screen. 50 */ 51 public final class BluetoothDevicePreference extends GearPreference implements 52 CachedBluetoothDevice.Callback { 53 private static final String TAG = "BluetoothDevicePref"; 54 55 private static int sDimAlpha = Integer.MIN_VALUE; 56 57 private final CachedBluetoothDevice mCachedDevice; 58 private final UserManager mUserManager; 59 private final boolean mShowDevicesWithoutNames; 60 61 private AlertDialog mDisconnectDialog; 62 private String contentDescription = null; 63 private boolean mHideSecondTarget = false; 64 @VisibleForTesting 65 boolean mNeedNotifyHierarchyChanged = false; 66 /* Talk-back descriptions for various BT icons */ 67 Resources mResources; 68 BluetoothDevicePreference(Context context, CachedBluetoothDevice cachedDevice, boolean showDeviceWithoutNames)69 public BluetoothDevicePreference(Context context, CachedBluetoothDevice cachedDevice, 70 boolean showDeviceWithoutNames) { 71 super(context, null); 72 mResources = getContext().getResources(); 73 mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); 74 mShowDevicesWithoutNames = showDeviceWithoutNames; 75 76 if (sDimAlpha == Integer.MIN_VALUE) { 77 TypedValue outValue = new TypedValue(); 78 context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); 79 sDimAlpha = (int) (outValue.getFloat() * 255); 80 } 81 82 mCachedDevice = cachedDevice; 83 mCachedDevice.registerCallback(this); 84 85 onDeviceAttributesChanged(); 86 } 87 setNeedNotifyHierarchyChanged(boolean needNotifyHierarchyChanged)88 public void setNeedNotifyHierarchyChanged(boolean needNotifyHierarchyChanged) { 89 mNeedNotifyHierarchyChanged = needNotifyHierarchyChanged; 90 } 91 92 @Override shouldHideSecondTarget()93 protected boolean shouldHideSecondTarget() { 94 return mCachedDevice == null 95 || mCachedDevice.getBondState() != BluetoothDevice.BOND_BONDED 96 || mUserManager.hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH) 97 || mHideSecondTarget; 98 } 99 100 @Override getSecondTargetResId()101 protected int getSecondTargetResId() { 102 return R.layout.preference_widget_gear; 103 } 104 getCachedDevice()105 CachedBluetoothDevice getCachedDevice() { 106 return mCachedDevice; 107 } 108 109 @Override onPrepareForRemoval()110 protected void onPrepareForRemoval() { 111 super.onPrepareForRemoval(); 112 mCachedDevice.unregisterCallback(this); 113 if (mDisconnectDialog != null) { 114 mDisconnectDialog.dismiss(); 115 mDisconnectDialog = null; 116 } 117 } 118 getBluetoothDevice()119 public CachedBluetoothDevice getBluetoothDevice() { 120 return mCachedDevice; 121 } 122 hideSecondTarget(boolean hideSecondTarget)123 public void hideSecondTarget(boolean hideSecondTarget) { 124 mHideSecondTarget = hideSecondTarget; 125 } 126 onDeviceAttributesChanged()127 public void onDeviceAttributesChanged() { 128 /* 129 * The preference framework takes care of making sure the value has 130 * changed before proceeding. It will also call notifyChanged() if 131 * any preference info has changed from the previous value. 132 */ 133 setTitle(mCachedDevice.getName()); 134 // Null check is done at the framework 135 setSummary(mCachedDevice.getConnectionSummary()); 136 137 final Pair<Drawable, String> pair = 138 BluetoothUtils.getBtRainbowDrawableWithDescription(getContext(), mCachedDevice); 139 if (pair.first != null) { 140 setIcon(pair.first); 141 contentDescription = pair.second; 142 } 143 144 // Used to gray out the item 145 setEnabled(!mCachedDevice.isBusy()); 146 147 // Device is only visible in the UI if it has a valid name besides MAC address or when user 148 // allows showing devices without user-friendly name in developer settings 149 setVisible(mShowDevicesWithoutNames || mCachedDevice.hasHumanReadableName()); 150 151 // This could affect ordering, so notify that 152 if (mNeedNotifyHierarchyChanged) { 153 notifyHierarchyChanged(); 154 } 155 } 156 157 @Override onBindViewHolder(PreferenceViewHolder view)158 public void onBindViewHolder(PreferenceViewHolder view) { 159 // Disable this view if the bluetooth enable/disable preference view is off 160 if (null != findPreferenceInHierarchy("bt_checkbox")) { 161 setDependency("bt_checkbox"); 162 } 163 164 if (mCachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) { 165 ImageView deviceDetails = (ImageView) view.findViewById(R.id.settings_button); 166 167 if (deviceDetails != null) { 168 deviceDetails.setOnClickListener(this); 169 } 170 } 171 final ImageView imageView = (ImageView) view.findViewById(android.R.id.icon); 172 if (imageView != null) { 173 imageView.setContentDescription(contentDescription); 174 // Set property to prevent Talkback from reading out. 175 imageView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 176 imageView.setElevation( 177 getContext().getResources().getDimension(R.dimen.bt_icon_elevation)); 178 } 179 super.onBindViewHolder(view); 180 } 181 182 @Override equals(Object o)183 public boolean equals(Object o) { 184 if ((o == null) || !(o instanceof BluetoothDevicePreference)) { 185 return false; 186 } 187 return mCachedDevice.equals( 188 ((BluetoothDevicePreference) o).mCachedDevice); 189 } 190 191 @Override hashCode()192 public int hashCode() { 193 return mCachedDevice.hashCode(); 194 } 195 196 @Override compareTo(Preference another)197 public int compareTo(Preference another) { 198 if (!(another instanceof BluetoothDevicePreference)) { 199 // Rely on default sort 200 return super.compareTo(another); 201 } 202 203 return mCachedDevice 204 .compareTo(((BluetoothDevicePreference) another).mCachedDevice); 205 } 206 onClicked()207 void onClicked() { 208 Context context = getContext(); 209 int bondState = mCachedDevice.getBondState(); 210 211 final MetricsFeatureProvider metricsFeatureProvider = 212 FeatureFactory.getFactory(context).getMetricsFeatureProvider(); 213 214 if (mCachedDevice.isConnected()) { 215 metricsFeatureProvider.action(context, 216 SettingsEnums.ACTION_SETTINGS_BLUETOOTH_DISCONNECT); 217 askDisconnect(); 218 } else if (bondState == BluetoothDevice.BOND_BONDED) { 219 metricsFeatureProvider.action(context, 220 SettingsEnums.ACTION_SETTINGS_BLUETOOTH_CONNECT); 221 mCachedDevice.connect(true); 222 } else if (bondState == BluetoothDevice.BOND_NONE) { 223 metricsFeatureProvider.action(context, 224 SettingsEnums.ACTION_SETTINGS_BLUETOOTH_PAIR); 225 if (!mCachedDevice.hasHumanReadableName()) { 226 metricsFeatureProvider.action(context, 227 SettingsEnums.ACTION_SETTINGS_BLUETOOTH_PAIR_DEVICES_WITHOUT_NAMES); 228 } 229 pair(); 230 } 231 } 232 233 // Show disconnect confirmation dialog for a device. askDisconnect()234 private void askDisconnect() { 235 Context context = getContext(); 236 String name = mCachedDevice.getName(); 237 if (TextUtils.isEmpty(name)) { 238 name = context.getString(R.string.bluetooth_device); 239 } 240 String message = context.getString(R.string.bluetooth_disconnect_all_profiles, name); 241 String title = context.getString(R.string.bluetooth_disconnect_title); 242 243 DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() { 244 public void onClick(DialogInterface dialog, int which) { 245 mCachedDevice.disconnect(); 246 } 247 }; 248 249 mDisconnectDialog = Utils.showDisconnectDialog(context, 250 mDisconnectDialog, disconnectListener, title, Html.fromHtml(message)); 251 } 252 pair()253 private void pair() { 254 if (!mCachedDevice.startPairing()) { 255 Utils.showError(getContext(), mCachedDevice.getName(), 256 R.string.bluetooth_pairing_error_message); 257 } 258 } 259 } 260