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