1 /* 2 * Copyright (C) 2018 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.accessibility; 18 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothHapClient; 21 import android.bluetooth.BluetoothHearingAid; 22 import android.bluetooth.BluetoothLeAudio; 23 import android.bluetooth.BluetoothProfile; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.text.TextUtils; 29 30 import androidx.annotation.VisibleForTesting; 31 import androidx.fragment.app.FragmentManager; 32 import androidx.preference.Preference; 33 import androidx.preference.PreferenceScreen; 34 35 import com.android.settings.R; 36 import com.android.settings.core.BasePreferenceController; 37 import com.android.settings.core.SubSettingLauncher; 38 import com.android.settingslib.bluetooth.BluetoothCallback; 39 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 40 import com.android.settingslib.bluetooth.HearingAidInfo; 41 import com.android.settingslib.bluetooth.LocalBluetoothManager; 42 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 43 import com.android.settingslib.core.lifecycle.LifecycleObserver; 44 import com.android.settingslib.core.lifecycle.events.OnStart; 45 import com.android.settingslib.core.lifecycle.events.OnStop; 46 import com.android.settingslib.utils.ThreadUtils; 47 48 import java.util.Set; 49 50 /** 51 * Controller that shows and updates the bluetooth device name 52 */ 53 public class AccessibilityHearingAidPreferenceController extends BasePreferenceController 54 implements LifecycleObserver, OnStart, OnStop, BluetoothCallback, 55 LocalBluetoothProfileManager.ServiceListener { 56 private static final String TAG = "AccessibilityHearingAidPreferenceController"; 57 private Preference mHearingAidPreference; 58 59 private final BroadcastReceiver mHearingAidChangedReceiver = new BroadcastReceiver() { 60 @Override 61 public void onReceive(Context context, Intent intent) { 62 updateState(mHearingAidPreference); 63 } 64 }; 65 66 private final LocalBluetoothManager mLocalBluetoothManager; 67 private final LocalBluetoothProfileManager mProfileManager; 68 private final HearingAidHelper mHelper; 69 private FragmentManager mFragmentManager; 70 AccessibilityHearingAidPreferenceController(Context context, String preferenceKey)71 public AccessibilityHearingAidPreferenceController(Context context, String preferenceKey) { 72 super(context, preferenceKey); 73 mLocalBluetoothManager = com.android.settings.bluetooth.Utils.getLocalBluetoothManager( 74 context); 75 mProfileManager = mLocalBluetoothManager.getProfileManager(); 76 mHelper = new HearingAidHelper(context); 77 } 78 79 @Override displayPreference(PreferenceScreen screen)80 public void displayPreference(PreferenceScreen screen) { 81 super.displayPreference(screen); 82 mHearingAidPreference = screen.findPreference(getPreferenceKey()); 83 } 84 85 @Override getAvailabilityStatus()86 public int getAvailabilityStatus() { 87 return mHelper.isHearingAidSupported() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; 88 } 89 90 @Override onStart()91 public void onStart() { 92 IntentFilter filter = new IntentFilter(); 93 filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); 94 filter.addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED); 95 filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED); 96 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 97 mContext.registerReceiver(mHearingAidChangedReceiver, filter); 98 mLocalBluetoothManager.getEventManager().registerCallback(this); 99 // Can't get connected hearing aids when hearing aids related profiles are not ready. The 100 // profiles will be ready after the services are connected. Needs to add listener and 101 // updates the information when all hearing aids related services are connected. 102 if (!mHelper.isAllHearingAidRelatedProfilesReady()) { 103 mProfileManager.addServiceListener(this); 104 } 105 } 106 107 @Override onStop()108 public void onStop() { 109 mContext.unregisterReceiver(mHearingAidChangedReceiver); 110 mLocalBluetoothManager.getEventManager().unregisterCallback(this); 111 mProfileManager.removeServiceListener(this); 112 } 113 114 @Override handlePreferenceTreeClick(Preference preference)115 public boolean handlePreferenceTreeClick(Preference preference) { 116 if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { 117 launchHearingAidPage(); 118 return true; 119 } 120 return false; 121 } 122 123 @Override refreshSummary(Preference preference)124 protected void refreshSummary(Preference preference) { 125 if (preference == null) { 126 return; 127 } 128 129 // Loading the hearing aids summary requires IPC call, which can block the UI thread. 130 // To reduce page loading latency, move loadSummary in the background thread. 131 ThreadUtils.postOnBackgroundThread(() -> { 132 CharSequence summary = loadSummary(); 133 ThreadUtils.getUiThreadHandler().post(() -> preference.setSummary(summary)); 134 }); 135 } 136 loadSummary()137 private CharSequence loadSummary() { 138 final CachedBluetoothDevice device = mHelper.getConnectedHearingAidDevice(); 139 if (device == null) { 140 return mContext.getText(R.string.accessibility_hearingaid_not_connected_summary); 141 } 142 143 final int connectedNum = getConnectedHearingAidDeviceNum(); 144 final CharSequence name = device.getName(); 145 if (connectedNum > 1) { 146 return mContext.getString(R.string.accessibility_hearingaid_more_device_summary, name); 147 } 148 149 // Check if another side of LE audio hearing aid is connected as a pair 150 final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice(); 151 if (memberDevices.stream().anyMatch(m -> m.getDevice().isConnected())) { 152 return mContext.getString( 153 R.string.accessibility_hearingaid_left_and_right_side_device_summary, 154 name); 155 } 156 157 // Check if another side of ASHA hearing aid is connected as a pair 158 final CachedBluetoothDevice subDevice = device.getSubDevice(); 159 if (subDevice != null && subDevice.getDevice().isConnected()) { 160 return mContext.getString( 161 R.string.accessibility_hearingaid_left_and_right_side_device_summary, name); 162 } 163 164 final int side = device.getDeviceSide(); 165 if (side == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) { 166 return mContext.getString( 167 R.string.accessibility_hearingaid_left_and_right_side_device_summary, name); 168 } else if (side == HearingAidInfo.DeviceSide.SIDE_LEFT) { 169 return mContext.getString( 170 R.string.accessibility_hearingaid_left_side_device_summary, name); 171 } else if (side == HearingAidInfo.DeviceSide.SIDE_RIGHT) { 172 return mContext.getString( 173 R.string.accessibility_hearingaid_right_side_device_summary, name); 174 } 175 176 // Invalid side 177 return mContext.getString( 178 R.string.accessibility_hearingaid_active_device_summary, name); 179 } 180 181 @Override onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile)182 public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { 183 if (activeDevice == null) { 184 return; 185 } 186 187 if (bluetoothProfile == BluetoothProfile.HEARING_AID) { 188 HearingAidUtils.launchHearingAidPairingDialog( 189 mFragmentManager, activeDevice, getMetricsCategory()); 190 } 191 } 192 193 @Override onServiceConnected()194 public void onServiceConnected() { 195 if (mHelper.isAllHearingAidRelatedProfilesReady()) { 196 updateState(mHearingAidPreference); 197 mProfileManager.removeServiceListener(this); 198 } 199 } 200 201 @Override onServiceDisconnected()202 public void onServiceDisconnected() { 203 // Do nothing 204 } 205 setFragmentManager(FragmentManager fragmentManager)206 public void setFragmentManager(FragmentManager fragmentManager) { 207 mFragmentManager = fragmentManager; 208 } 209 getConnectedHearingAidDeviceNum()210 private int getConnectedHearingAidDeviceNum() { 211 return mHelper.getConnectedHearingAidDeviceList().size(); 212 } 213 214 @VisibleForTesting(otherwise = VisibleForTesting.NONE) setPreference(Preference preference)215 void setPreference(Preference preference) { 216 mHearingAidPreference = preference; 217 } 218 launchHearingAidPage()219 private void launchHearingAidPage() { 220 new SubSettingLauncher(mContext) 221 .setDestination(AccessibilityHearingAidsFragment.class.getName()) 222 .setSourceMetricsCategory(getMetricsCategory()) 223 .launch(); 224 } 225 }