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