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