1 /* 2 * Copyright (C) 2011 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 android.app.AlertDialog; 20 import android.app.Dialog; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothProfile; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.os.Bundle; 26 import android.support.annotation.VisibleForTesting; 27 import android.text.Html; 28 import android.text.TextUtils; 29 import android.util.Log; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.View.OnClickListener; 33 import android.view.ViewGroup; 34 import android.widget.CheckBox; 35 import android.widget.EditText; 36 import android.widget.TextView; 37 38 import com.android.internal.logging.nano.MetricsProto; 39 import com.android.settings.R; 40 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 41 import com.android.settingslib.bluetooth.A2dpProfile; 42 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 43 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 44 import com.android.settingslib.bluetooth.LocalBluetoothManager; 45 import com.android.settingslib.bluetooth.LocalBluetoothProfile; 46 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 47 import com.android.settingslib.bluetooth.MapProfile; 48 import com.android.settingslib.bluetooth.PanProfile; 49 import com.android.settingslib.bluetooth.PbapServerProfile; 50 51 public final class DeviceProfilesSettings extends InstrumentedDialogFragment implements 52 CachedBluetoothDevice.Callback, DialogInterface.OnClickListener, OnClickListener { 53 private static final String TAG = "DeviceProfilesSettings"; 54 55 public static final String ARG_DEVICE_ADDRESS = "device_address"; 56 57 private static final String KEY_PROFILE_CONTAINER = "profile_container"; 58 private static final String KEY_UNPAIR = "unpair"; 59 private static final String KEY_PBAP_SERVER = "PBAP Server"; 60 @VisibleForTesting 61 static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio"; 62 63 private CachedBluetoothDevice mCachedDevice; 64 private LocalBluetoothManager mManager; 65 private LocalBluetoothProfileManager mProfileManager; 66 67 private ViewGroup mProfileContainer; 68 private TextView mProfileLabel; 69 70 private AlertDialog mDisconnectDialog; 71 private boolean mProfileGroupIsRemoved; 72 73 private View mRootView; 74 75 @Override getMetricsCategory()76 public int getMetricsCategory() { 77 return MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_PAIRED_DEVICE_PROFILE; 78 } 79 80 @Override onCreate(Bundle savedInstanceState)81 public void onCreate(Bundle savedInstanceState) { 82 super.onCreate(savedInstanceState); 83 84 mManager = Utils.getLocalBtManager(getActivity()); 85 CachedBluetoothDeviceManager deviceManager = mManager.getCachedDeviceManager(); 86 87 String address = getArguments().getString(ARG_DEVICE_ADDRESS); 88 BluetoothDevice remoteDevice = mManager.getBluetoothAdapter().getRemoteDevice(address); 89 90 mCachedDevice = deviceManager.findDevice(remoteDevice); 91 if (mCachedDevice == null) { 92 mCachedDevice = deviceManager.addDevice(mManager.getBluetoothAdapter(), 93 mManager.getProfileManager(), remoteDevice); 94 } 95 mProfileManager = mManager.getProfileManager(); 96 } 97 98 @Override onCreateDialog(Bundle savedInstanceState)99 public Dialog onCreateDialog(Bundle savedInstanceState) { 100 mRootView = LayoutInflater.from(getContext()).inflate(R.layout.device_profiles_settings, 101 null); 102 mProfileContainer = (ViewGroup) mRootView.findViewById(R.id.profiles_section); 103 mProfileLabel = (TextView) mRootView.findViewById(R.id.profiles_label); 104 final EditText deviceName = (EditText) mRootView.findViewById(R.id.name); 105 deviceName.setText(mCachedDevice.getName(), TextView.BufferType.EDITABLE); 106 return new AlertDialog.Builder(getContext()) 107 .setView(mRootView) 108 .setNeutralButton(R.string.forget, this) 109 .setPositiveButton(R.string.okay, this) 110 .setTitle(R.string.bluetooth_preference_paired_devices) 111 .create(); 112 } 113 114 @Override onClick(DialogInterface dialog, int which)115 public void onClick(DialogInterface dialog, int which) { 116 switch (which) { 117 case DialogInterface.BUTTON_POSITIVE: 118 EditText deviceName = (EditText) mRootView.findViewById(R.id.name); 119 mCachedDevice.setName(deviceName.getText().toString()); 120 break; 121 case DialogInterface.BUTTON_NEUTRAL: 122 mCachedDevice.unpair(); 123 break; 124 } 125 } 126 127 @Override onDestroy()128 public void onDestroy() { 129 super.onDestroy(); 130 if (mDisconnectDialog != null) { 131 mDisconnectDialog.dismiss(); 132 mDisconnectDialog = null; 133 } 134 if (mCachedDevice != null) { 135 mCachedDevice.unregisterCallback(this); 136 } 137 } 138 139 @Override onSaveInstanceState(Bundle outState)140 public void onSaveInstanceState(Bundle outState) { 141 super.onSaveInstanceState(outState); 142 } 143 144 @Override onResume()145 public void onResume() { 146 super.onResume(); 147 148 mManager.setForegroundActivity(getActivity()); 149 if (mCachedDevice != null) { 150 mCachedDevice.registerCallback(this); 151 if (mCachedDevice.getBondState() == BluetoothDevice.BOND_NONE) { 152 dismiss(); 153 return; 154 } 155 addPreferencesForProfiles(); 156 refresh(); 157 } 158 } 159 160 @Override onPause()161 public void onPause() { 162 super.onPause(); 163 164 if (mCachedDevice != null) { 165 mCachedDevice.unregisterCallback(this); 166 } 167 168 mManager.setForegroundActivity(null); 169 } 170 addPreferencesForProfiles()171 private void addPreferencesForProfiles() { 172 mProfileContainer.removeAllViews(); 173 for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) { 174 CheckBox pref = createProfilePreference(profile); 175 // MAP and PBAP profiles would be added based on permission access 176 if (!((profile instanceof PbapServerProfile) || 177 (profile instanceof MapProfile))) { 178 mProfileContainer.addView(pref); 179 } 180 181 if (profile instanceof A2dpProfile) { 182 BluetoothDevice device = mCachedDevice.getDevice(); 183 A2dpProfile a2dpProfile = (A2dpProfile) profile; 184 if (a2dpProfile.supportsHighQualityAudio(device)) { 185 CheckBox highQualityPref = new CheckBox(getActivity()); 186 highQualityPref.setTag(HIGH_QUALITY_AUDIO_PREF_TAG); 187 highQualityPref.setOnClickListener(v -> { 188 a2dpProfile.setHighQualityAudioEnabled(device, highQualityPref.isChecked()); 189 }); 190 highQualityPref.setVisibility(View.GONE); 191 mProfileContainer.addView(highQualityPref); 192 } 193 refreshProfilePreference(pref, profile); 194 } 195 } 196 197 final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); 198 Log.d(TAG, "addPreferencesForProfiles: pbapPermission = " + pbapPermission); 199 // Only provide PBAP cabability if the client device has requested PBAP. 200 if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { 201 final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); 202 CheckBox pbapPref = createProfilePreference(psp); 203 mProfileContainer.addView(pbapPref); 204 } 205 206 final MapProfile mapProfile = mManager.getProfileManager().getMapProfile(); 207 final int mapPermission = mCachedDevice.getMessagePermissionChoice(); 208 Log.d(TAG, "addPreferencesForProfiles: mapPermission = " + mapPermission); 209 if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { 210 CheckBox mapPreference = createProfilePreference(mapProfile); 211 mProfileContainer.addView(mapPreference); 212 } 213 214 showOrHideProfileGroup(); 215 } 216 showOrHideProfileGroup()217 private void showOrHideProfileGroup() { 218 int numProfiles = mProfileContainer.getChildCount(); 219 if (!mProfileGroupIsRemoved && numProfiles == 0) { 220 mProfileContainer.setVisibility(View.GONE); 221 mProfileLabel.setVisibility(View.GONE); 222 mProfileGroupIsRemoved = true; 223 } else if (mProfileGroupIsRemoved && numProfiles != 0) { 224 mProfileContainer.setVisibility(View.VISIBLE); 225 mProfileLabel.setVisibility(View.VISIBLE); 226 mProfileGroupIsRemoved = false; 227 } 228 } 229 230 /** 231 * Creates a checkbox preference for the particular profile. The key will be 232 * the profile's name. 233 * 234 * @param profile The profile for which the preference controls. 235 * @return A preference that allows the user to choose whether this profile 236 * will be connected to. 237 */ createProfilePreference(LocalBluetoothProfile profile)238 private CheckBox createProfilePreference(LocalBluetoothProfile profile) { 239 CheckBox pref = new CheckBox(getActivity()); 240 pref.setTag(profile.toString()); 241 pref.setText(profile.getNameResource(mCachedDevice.getDevice())); 242 pref.setOnClickListener(this); 243 244 refreshProfilePreference(pref, profile); 245 246 return pref; 247 } 248 249 @Override onClick(View v)250 public void onClick(View v) { 251 if (v instanceof CheckBox) { 252 LocalBluetoothProfile prof = getProfileOf(v); 253 onProfileClicked(prof, (CheckBox) v); 254 } 255 } 256 onProfileClicked(LocalBluetoothProfile profile, CheckBox profilePref)257 private void onProfileClicked(LocalBluetoothProfile profile, CheckBox profilePref) { 258 BluetoothDevice device = mCachedDevice.getDevice(); 259 260 if (!profilePref.isChecked()) { 261 // Recheck it, until the dialog is done. 262 profilePref.setChecked(true); 263 askDisconnect(mManager.getForegroundActivity(), profile); 264 } else { 265 if (profile instanceof MapProfile) { 266 mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED); 267 } 268 if (profile instanceof PbapServerProfile) { 269 mCachedDevice.setPhonebookPermissionChoice(BluetoothDevice.ACCESS_ALLOWED); 270 refreshProfilePreference(profilePref, profile); 271 // PBAP server is not preffered profile and cannot initiate connection, so return 272 return; 273 } 274 if (profile.isPreferred(device)) { 275 // profile is preferred but not connected: disable auto-connect 276 if (profile instanceof PanProfile) { 277 mCachedDevice.connectProfile(profile); 278 } else { 279 profile.setPreferred(device, false); 280 } 281 } else { 282 profile.setPreferred(device, true); 283 mCachedDevice.connectProfile(profile); 284 } 285 refreshProfilePreference(profilePref, profile); 286 } 287 } 288 askDisconnect(Context context, final LocalBluetoothProfile profile)289 private void askDisconnect(Context context, 290 final LocalBluetoothProfile profile) { 291 // local reference for callback 292 final CachedBluetoothDevice device = mCachedDevice; 293 String name = device.getName(); 294 if (TextUtils.isEmpty(name)) { 295 name = context.getString(R.string.bluetooth_device); 296 } 297 298 String profileName = context.getString(profile.getNameResource(device.getDevice())); 299 300 String title = context.getString(R.string.bluetooth_disable_profile_title); 301 String message = context.getString(R.string.bluetooth_disable_profile_message, 302 profileName, name); 303 304 DialogInterface.OnClickListener disconnectListener = 305 new DialogInterface.OnClickListener() { 306 public void onClick(DialogInterface dialog, int which) { 307 308 // Disconnect only when user has selected OK otherwise ignore 309 if (which == DialogInterface.BUTTON_POSITIVE) { 310 device.disconnect(profile); 311 profile.setPreferred(device.getDevice(), false); 312 if (profile instanceof MapProfile) { 313 device.setMessagePermissionChoice(BluetoothDevice.ACCESS_REJECTED); 314 } 315 if (profile instanceof PbapServerProfile) { 316 device.setPhonebookPermissionChoice(BluetoothDevice.ACCESS_REJECTED); 317 } 318 } 319 refreshProfilePreference(findProfile(profile.toString()), profile); 320 } 321 }; 322 323 mDisconnectDialog = Utils.showDisconnectDialog(context, 324 mDisconnectDialog, disconnectListener, title, Html.fromHtml(message)); 325 } 326 327 @Override onDeviceAttributesChanged()328 public void onDeviceAttributesChanged() { 329 refresh(); 330 } 331 refresh()332 private void refresh() { 333 final EditText deviceNameField = (EditText) mRootView.findViewById(R.id.name); 334 if (deviceNameField != null) { 335 deviceNameField.setText(mCachedDevice.getName()); 336 com.android.settings.Utils.setEditTextCursorPosition(deviceNameField); 337 } 338 339 refreshProfiles(); 340 } 341 refreshProfiles()342 private void refreshProfiles() { 343 for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) { 344 CheckBox profilePref = findProfile(profile.toString()); 345 if (profilePref == null) { 346 profilePref = createProfilePreference(profile); 347 mProfileContainer.addView(profilePref); 348 } else { 349 refreshProfilePreference(profilePref, profile); 350 } 351 } 352 for (LocalBluetoothProfile profile : mCachedDevice.getRemovedProfiles()) { 353 CheckBox profilePref = findProfile(profile.toString()); 354 if (profilePref != null) { 355 356 if (profile instanceof PbapServerProfile) { 357 final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); 358 Log.d(TAG, "refreshProfiles: pbapPermission = " + pbapPermission); 359 if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) 360 continue; 361 } 362 if (profile instanceof MapProfile) { 363 final int mapPermission = mCachedDevice.getMessagePermissionChoice(); 364 Log.d(TAG, "refreshProfiles: mapPermission = " + mapPermission); 365 if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) 366 continue; 367 } 368 Log.d(TAG, "Removing " + profile.toString() + " from profile list"); 369 mProfileContainer.removeView(profilePref); 370 } 371 } 372 373 showOrHideProfileGroup(); 374 } 375 findProfile(String profile)376 private CheckBox findProfile(String profile) { 377 return (CheckBox) mProfileContainer.findViewWithTag(profile); 378 } 379 refreshProfilePreference(CheckBox profilePref, LocalBluetoothProfile profile)380 private void refreshProfilePreference(CheckBox profilePref, 381 LocalBluetoothProfile profile) { 382 BluetoothDevice device = mCachedDevice.getDevice(); 383 384 // Gray out checkbox while connecting and disconnecting. 385 profilePref.setEnabled(!mCachedDevice.isBusy()); 386 387 if (profile instanceof MapProfile) { 388 profilePref.setChecked(mCachedDevice.getMessagePermissionChoice() 389 == CachedBluetoothDevice.ACCESS_ALLOWED); 390 391 } else if (profile instanceof PbapServerProfile) { 392 profilePref.setChecked(mCachedDevice.getPhonebookPermissionChoice() 393 == CachedBluetoothDevice.ACCESS_ALLOWED); 394 395 } else if (profile instanceof PanProfile) { 396 profilePref.setChecked(profile.getConnectionStatus(device) == 397 BluetoothProfile.STATE_CONNECTED); 398 399 } else { 400 profilePref.setChecked(profile.isPreferred(device)); 401 } 402 if (profile instanceof A2dpProfile) { 403 A2dpProfile a2dpProfile = (A2dpProfile) profile; 404 View v = mProfileContainer.findViewWithTag(HIGH_QUALITY_AUDIO_PREF_TAG); 405 if (v instanceof CheckBox) { 406 CheckBox highQualityPref = (CheckBox) v; 407 highQualityPref.setText(a2dpProfile.getHighQualityAudioOptionLabel(device)); 408 highQualityPref.setChecked(a2dpProfile.isHighQualityAudioEnabled(device)); 409 410 if (a2dpProfile.isPreferred(device)) { 411 v.setVisibility(View.VISIBLE); 412 v.setEnabled(!mCachedDevice.isBusy()); 413 } else { 414 v.setVisibility(View.GONE); 415 } 416 } 417 } 418 } 419 getProfileOf(View v)420 private LocalBluetoothProfile getProfileOf(View v) { 421 if (!(v instanceof CheckBox)) { 422 return null; 423 } 424 String key = (String) v.getTag(); 425 if (TextUtils.isEmpty(key)) return null; 426 427 try { 428 return mProfileManager.getProfileByName(key); 429 } catch (IllegalArgumentException ignored) { 430 return null; 431 } 432 } 433 } 434