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 }