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 76 public int getMetricsCategory() { 77 return MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_PAIRED_DEVICE_PROFILE; 78 } 79 80 @Override 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 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 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 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 140 public void onSaveInstanceState(Bundle outState) { 141 super.onSaveInstanceState(outState); 142 } 143 144 @Override 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 161 public void onPause() { 162 super.onPause(); 163 164 if (mCachedDevice != null) { 165 mCachedDevice.unregisterCallback(this); 166 } 167 168 mManager.setForegroundActivity(null); 169 } 170 171 private void addPreferencesForProfiles() { 172 mProfileContainer.removeAllViews(); 173 for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) { 174 CheckBox pref = createProfilePreference(profile); 175 mProfileContainer.addView(pref); 176 177 if (profile instanceof A2dpProfile) { 178 BluetoothDevice device = mCachedDevice.getDevice(); 179 A2dpProfile a2dpProfile = (A2dpProfile) profile; 180 if (a2dpProfile.supportsHighQualityAudio(device)) { 181 CheckBox highQualityPref = new CheckBox(getActivity()); 182 highQualityPref.setTag(HIGH_QUALITY_AUDIO_PREF_TAG); 183 highQualityPref.setOnClickListener(v -> { 184 a2dpProfile.setHighQualityAudioEnabled(device, highQualityPref.isChecked()); 185 }); 186 highQualityPref.setVisibility(View.GONE); 187 mProfileContainer.addView(highQualityPref); 188 } 189 refreshProfilePreference(pref, profile); 190 } 191 } 192 193 final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); 194 // Only provide PBAP cabability if the client device has requested PBAP. 195 if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { 196 final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); 197 CheckBox pbapPref = createProfilePreference(psp); 198 mProfileContainer.addView(pbapPref); 199 } 200 201 final MapProfile mapProfile = mManager.getProfileManager().getMapProfile(); 202 final int mapPermission = mCachedDevice.getMessagePermissionChoice(); 203 if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { 204 CheckBox mapPreference = createProfilePreference(mapProfile); 205 mProfileContainer.addView(mapPreference); 206 } 207 208 showOrHideProfileGroup(); 209 } 210 211 private void showOrHideProfileGroup() { 212 int numProfiles = mProfileContainer.getChildCount(); 213 if (!mProfileGroupIsRemoved && numProfiles == 0) { 214 mProfileContainer.setVisibility(View.GONE); 215 mProfileLabel.setVisibility(View.GONE); 216 mProfileGroupIsRemoved = true; 217 } else if (mProfileGroupIsRemoved && numProfiles != 0) { 218 mProfileContainer.setVisibility(View.VISIBLE); 219 mProfileLabel.setVisibility(View.VISIBLE); 220 mProfileGroupIsRemoved = false; 221 } 222 } 223 224 /** 225 * Creates a checkbox preference for the particular profile. The key will be 226 * the profile's name. 227 * 228 * @param profile The profile for which the preference controls. 229 * @return A preference that allows the user to choose whether this profile 230 * will be connected to. 231 */ 232 private CheckBox createProfilePreference(LocalBluetoothProfile profile) { 233 CheckBox pref = new CheckBox(getActivity()); 234 pref.setTag(profile.toString()); 235 pref.setText(profile.getNameResource(mCachedDevice.getDevice())); 236 pref.setOnClickListener(this); 237 238 refreshProfilePreference(pref, profile); 239 240 return pref; 241 } 242 243 @Override 244 public void onClick(View v) { 245 if (v instanceof CheckBox) { 246 LocalBluetoothProfile prof = getProfileOf(v); 247 onProfileClicked(prof, (CheckBox) v); 248 } 249 } 250 251 private void onProfileClicked(LocalBluetoothProfile profile, CheckBox profilePref) { 252 BluetoothDevice device = mCachedDevice.getDevice(); 253 254 if (KEY_PBAP_SERVER.equals(profilePref.getTag())) { 255 final int newPermission = mCachedDevice.getPhonebookPermissionChoice() 256 == CachedBluetoothDevice.ACCESS_ALLOWED ? CachedBluetoothDevice.ACCESS_REJECTED 257 : CachedBluetoothDevice.ACCESS_ALLOWED; 258 mCachedDevice.setPhonebookPermissionChoice(newPermission); 259 profilePref.setChecked(newPermission == CachedBluetoothDevice.ACCESS_ALLOWED); 260 return; 261 } 262 263 if (!profilePref.isChecked()) { 264 // Recheck it, until the dialog is done. 265 profilePref.setChecked(true); 266 askDisconnect(mManager.getForegroundActivity(), profile); 267 } else { 268 if (profile instanceof MapProfile) { 269 mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED); 270 } 271 if (profile.isPreferred(device)) { 272 // profile is preferred but not connected: disable auto-connect 273 if (profile instanceof PanProfile) { 274 mCachedDevice.connectProfile(profile); 275 } else { 276 profile.setPreferred(device, false); 277 } 278 } else { 279 profile.setPreferred(device, true); 280 mCachedDevice.connectProfile(profile); 281 } 282 refreshProfilePreference(profilePref, profile); 283 } 284 } 285 286 private void askDisconnect(Context context, 287 final LocalBluetoothProfile profile) { 288 // local reference for callback 289 final CachedBluetoothDevice device = mCachedDevice; 290 String name = device.getName(); 291 if (TextUtils.isEmpty(name)) { 292 name = context.getString(R.string.bluetooth_device); 293 } 294 295 String profileName = context.getString(profile.getNameResource(device.getDevice())); 296 297 String title = context.getString(R.string.bluetooth_disable_profile_title); 298 String message = context.getString(R.string.bluetooth_disable_profile_message, 299 profileName, name); 300 301 DialogInterface.OnClickListener disconnectListener = 302 new DialogInterface.OnClickListener() { 303 public void onClick(DialogInterface dialog, int which) { 304 device.disconnect(profile); 305 profile.setPreferred(device.getDevice(), false); 306 if (profile instanceof MapProfile) { 307 device.setMessagePermissionChoice(BluetoothDevice.ACCESS_REJECTED); 308 } 309 refreshProfilePreference(findProfile(profile.toString()), profile); 310 } 311 }; 312 313 mDisconnectDialog = Utils.showDisconnectDialog(context, 314 mDisconnectDialog, disconnectListener, title, Html.fromHtml(message)); 315 } 316 317 @Override 318 public void onDeviceAttributesChanged() { 319 refresh(); 320 } 321 322 private void refresh() { 323 final EditText deviceNameField = (EditText) mRootView.findViewById(R.id.name); 324 if (deviceNameField != null) { 325 deviceNameField.setText(mCachedDevice.getName()); 326 } 327 328 refreshProfiles(); 329 } 330 331 private void refreshProfiles() { 332 for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) { 333 CheckBox profilePref = findProfile(profile.toString()); 334 if (profilePref == null) { 335 profilePref = createProfilePreference(profile); 336 mProfileContainer.addView(profilePref); 337 } else { 338 refreshProfilePreference(profilePref, profile); 339 } 340 } 341 for (LocalBluetoothProfile profile : mCachedDevice.getRemovedProfiles()) { 342 CheckBox profilePref = findProfile(profile.toString()); 343 if (profilePref != null) { 344 Log.d(TAG, "Removing " + profile.toString() + " from profile list"); 345 mProfileContainer.removeView(profilePref); 346 } 347 } 348 349 showOrHideProfileGroup(); 350 } 351 352 private CheckBox findProfile(String profile) { 353 return (CheckBox) mProfileContainer.findViewWithTag(profile); 354 } 355 356 private void refreshProfilePreference(CheckBox profilePref, 357 LocalBluetoothProfile profile) { 358 BluetoothDevice device = mCachedDevice.getDevice(); 359 360 // Gray out checkbox while connecting and disconnecting. 361 profilePref.setEnabled(!mCachedDevice.isBusy()); 362 363 if (profile instanceof MapProfile) { 364 profilePref.setChecked(mCachedDevice.getMessagePermissionChoice() 365 == CachedBluetoothDevice.ACCESS_ALLOWED); 366 367 } else if (profile instanceof PbapServerProfile) { 368 profilePref.setChecked(mCachedDevice.getPhonebookPermissionChoice() 369 == CachedBluetoothDevice.ACCESS_ALLOWED); 370 371 } else if (profile instanceof PanProfile) { 372 profilePref.setChecked(profile.getConnectionStatus(device) == 373 BluetoothProfile.STATE_CONNECTED); 374 375 } else { 376 profilePref.setChecked(profile.isPreferred(device)); 377 } 378 if (profile instanceof A2dpProfile) { 379 A2dpProfile a2dpProfile = (A2dpProfile) profile; 380 View v = mProfileContainer.findViewWithTag(HIGH_QUALITY_AUDIO_PREF_TAG); 381 if (v instanceof CheckBox) { 382 CheckBox highQualityPref = (CheckBox) v; 383 highQualityPref.setText(a2dpProfile.getHighQualityAudioOptionLabel(device)); 384 highQualityPref.setChecked(a2dpProfile.isHighQualityAudioEnabled(device)); 385 386 if (a2dpProfile.isPreferred(device)) { 387 v.setVisibility(View.VISIBLE); 388 v.setEnabled(!mCachedDevice.isBusy()); 389 } else { 390 v.setVisibility(View.GONE); 391 } 392 } 393 } 394 } 395 396 private LocalBluetoothProfile getProfileOf(View v) { 397 if (!(v instanceof CheckBox)) { 398 return null; 399 } 400 String key = (String) v.getTag(); 401 if (TextUtils.isEmpty(key)) return null; 402 403 try { 404 return mProfileManager.getProfileByName(key); 405 } catch (IllegalArgumentException ignored) { 406 return null; 407 } 408 } 409 } 410