summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Chaitanya Cheemala (xWF) <ccheemala@google.com> 2025-01-08 04:55:59 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-01-08 04:55:59 -0800
commitca19e7c104c31473938332885ed73a2558d7f730 (patch)
tree4d0603a4bc131ebdf06f597b735006ab941df7eb
parentd05661f1d99a824d68f2950fc36c32b698eff787 (diff)
Revert^2 "[HA Input] Show notification to hint user can change input device when calling"
This reverts commit d05661f1d99a824d68f2950fc36c32b698eff787. Reason for revert: CL owner requested to revert this in order to fix the bug b/388436864 with their existing fix. Thanks! Change-Id: Ica69286f6bc55366ef9e979279608d8c951df5e6
-rw-r--r--core/java/com/android/internal/notification/SystemNotificationChannels.java8
-rw-r--r--core/res/res/values/strings.xml17
-rw-r--r--core/res/res/values/symbols.xml8
-rw-r--r--core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java5
-rw-r--r--proto/src/system_messages.proto4
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java18
-rw-r--r--services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java356
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java187
8 files changed, 601 insertions, 2 deletions
diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java
index 4aebde536dcf..5635943cb76e 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -67,6 +67,7 @@ public class SystemNotificationChannels {
@Deprecated public static final String SYSTEM_CHANGES_DEPRECATED = "SYSTEM_CHANGES";
public static final String SYSTEM_CHANGES = "SYSTEM_CHANGES_ALERTS";
public static final String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION";
+ public static final String ACCESSIBILITY_HEARING_DEVICE = "ACCESSIBILITY_HEARING_DEVICE";
public static final String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY";
public static final String ABUSIVE_BACKGROUND_APPS = "ABUSIVE_BACKGROUND_APPS";
@@ -203,6 +204,13 @@ public class SystemNotificationChannels {
newFeaturePrompt.setBlockable(true);
channelsList.add(newFeaturePrompt);
+ final NotificationChannel accessibilityHearingDeviceChannel = new NotificationChannel(
+ ACCESSIBILITY_HEARING_DEVICE,
+ context.getString(R.string.notification_channel_accessibility_hearing_device),
+ NotificationManager.IMPORTANCE_HIGH);
+ accessibilityHearingDeviceChannel.setBlockable(true);
+ channelsList.add(accessibilityHearingDeviceChannel);
+
final NotificationChannel accessibilitySecurityPolicyChannel = new NotificationChannel(
ACCESSIBILITY_SECURITY_POLICY,
context.getString(R.string.notification_channel_accessibility_security_policy),
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6313054e47f5..ad9e7252c6a8 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -881,6 +881,10 @@
<string name="notification_channel_accessibility_magnification">Magnification</string>
<!-- Text shown when viewing channel settings for notifications related to accessibility
+ hearing device. [CHAR_LIMIT=NONE]-->
+ <string name="notification_channel_accessibility_hearing_device">Hearing device</string>
+
+ <!-- Text shown when viewing channel settings for notifications related to accessibility
security policy. [CHAR_LIMIT=NONE]-->
<string name="notification_channel_accessibility_security_policy">Accessibility usage</string>
@@ -4985,6 +4989,19 @@
<!-- Text used to describe system navigation features, shown within a UI allowing a user to assign system magnification features to the Accessibility button in the navigation bar. -->
<string name="accessibility_magnification_chooser_text">Magnification</string>
+ <!-- Notification title for switching input to the phone's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_switch_phone_mic_notification_title">Switch to phone mic?</string>
+ <!-- Notification title for switching input to the hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_switch_hearing_mic_notification_title">Switch to hearing aid mic?</string>
+ <!-- Notification content for switching input to the phone's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_switch_phone_mic_notification_text">For better sound or if your hearing aid battery is low. This only switches your mic during the call.</string>
+ <!-- Notification content for switching input to the hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_switch_hearing_mic_notification_text">You can use your hearing aid microphone for hands-free calling. This only switches your mic during the call.</string>
+ <!-- Notification action button. Click it will switch the input between phone's microphone and hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_notification_switch_button">Switch</string>
+ <!-- Notification action button. Click it will open the bluetooth device details page for this hearing device. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] -->
+ <string name="hearing_device_notification_settings_button">Settings</string>
+
<!-- Text spoken when the current user is switched if accessibility is enabled. [CHAR LIMIT=none] -->
<string name="user_switched">Current user <xliff:g id="name" example="Bob">%1$s</xliff:g>.</string>
<!-- Message shown when switching to a user [CHAR LIMIT=none] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index a18f923d625b..9634da6e2c83 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3839,6 +3839,13 @@
<java-symbol type="string" name="reduce_bright_colors_feature_name" />
<java-symbol type="string" name="one_handed_mode_feature_name" />
+ <java-symbol type="string" name="hearing_device_switch_phone_mic_notification_title" />
+ <java-symbol type="string" name="hearing_device_switch_hearing_mic_notification_title" />
+ <java-symbol type="string" name="hearing_device_switch_phone_mic_notification_text" />
+ <java-symbol type="string" name="hearing_device_switch_hearing_mic_notification_text" />
+ <java-symbol type="string" name="hearing_device_notification_switch_button" />
+ <java-symbol type="string" name="hearing_device_notification_settings_button" />
+
<!-- com.android.internal.widget.RecyclerView -->
<java-symbol type="id" name="item_touch_helper_previous_elevation"/>
<java-symbol type="dimen" name="item_touch_helper_max_drag_scroll_per_frame"/>
@@ -4025,6 +4032,7 @@
<java-symbol type="string" name="notification_channel_heavy_weight_app" />
<java-symbol type="string" name="notification_channel_system_changes" />
<java-symbol type="string" name="notification_channel_accessibility_magnification" />
+ <java-symbol type="string" name="notification_channel_accessibility_hearing_device" />
<java-symbol type="string" name="notification_channel_accessibility_security_policy" />
<java-symbol type="string" name="notification_channel_display" />
<java-symbol type="string" name="config_defaultAutofillService" />
diff --git a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
index 0bf406c970f2..2bd3f4df9435 100644
--- a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
+++ b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
@@ -17,6 +17,7 @@
package com.android.internal.notification;
import static com.android.internal.notification.SystemNotificationChannels.ABUSIVE_BACKGROUND_APPS;
+import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_HEARING_DEVICE;
import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_MAGNIFICATION;
import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_SECURITY_POLICY;
import static com.android.internal.notification.SystemNotificationChannels.ACCOUNT;
@@ -90,8 +91,8 @@ public class SystemNotificationChannelsTest {
DEVELOPER_IMPORTANT, UPDATES, NETWORK_STATUS, NETWORK_ALERTS,
NETWORK_AVAILABLE, VPN, DEVICE_ADMIN, ALERTS, RETAIL_MODE, USB,
FOREGROUND_SERVICE, HEAVY_WEIGHT_APP, SYSTEM_CHANGES,
- ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_SECURITY_POLICY,
- ABUSIVE_BACKGROUND_APPS);
+ ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_HEARING_DEVICE,
+ ACCESSIBILITY_SECURITY_POLICY, ABUSIVE_BACKGROUND_APPS);
}
@Test
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index 648990588d29..3a38152825c9 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -420,5 +420,9 @@ message SystemMessage {
// Notify the user that accessibility floating menu is hidden.
// Package: com.android.systemui
NOTE_A11Y_FLOATING_MENU_HIDDEN = 1009;
+
+ // Notify the hearing aid user that input device can be changed to builtin device or hearing device.
+ // Package: android
+ NOTE_HEARING_DEVICE_INPUT_SWITCH = 1012;
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 37d045bf6422..6cd1f721d215 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -413,6 +413,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private SparseArray<SurfaceControl> mA11yOverlayLayers = new SparseArray<>();
private final FlashNotificationsController mFlashNotificationsController;
+ private final HearingDevicePhoneCallNotificationController mHearingDeviceNotificationController;
private final UserManagerInternal mUmi;
private AccessibilityUserState getCurrentUserStateLocked() {
@@ -569,6 +570,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
// TODO(b/255426725): not used on tests
mVisibleBgUserIds = null;
mInputManager = context.getSystemService(InputManager.class);
+ if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
+ mHearingDeviceNotificationController = new HearingDevicePhoneCallNotificationController(
+ context);
+ } else {
+ mHearingDeviceNotificationController = null;
+ }
init();
}
@@ -618,6 +625,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
} else {
mVisibleBgUserIds = null;
}
+ if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
+ mHearingDeviceNotificationController = new HearingDevicePhoneCallNotificationController(
+ context);
+ } else {
+ mHearingDeviceNotificationController = null;
+ }
init();
}
@@ -630,6 +643,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
if (enableTalkbackAndMagnifierKeyGestures()) {
mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler);
}
+ if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) {
+ if (mHearingDeviceNotificationController != null) {
+ mHearingDeviceNotificationController.startListenForCallState();
+ }
+ }
disableAccessibilityMenuToMigrateIfNeeded();
}
diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
new file mode 100644
index 000000000000..d06daf5db127
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.MediaRecorder;
+import android.os.Bundle;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.R;
+import com.android.internal.messages.nano.SystemMessageProto;
+import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * A controller class to handle notification for hearing device during phone calls.
+ */
+public class HearingDevicePhoneCallNotificationController {
+
+ private final TelephonyManager mTelephonyManager;
+ private final TelephonyCallback mTelephonyListener;
+ private final Executor mCallbackExecutor;
+
+ public HearingDevicePhoneCallNotificationController(@NonNull Context context) {
+ mTelephonyListener = new CallStateListener(context);
+ mTelephonyManager = context.getSystemService(TelephonyManager.class);
+ mCallbackExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @VisibleForTesting
+ HearingDevicePhoneCallNotificationController(@NonNull Context context,
+ TelephonyCallback telephonyCallback) {
+ mTelephonyListener = telephonyCallback;
+ mTelephonyManager = context.getSystemService(TelephonyManager.class);
+ mCallbackExecutor = context.getMainExecutor();
+ }
+
+ /**
+ * Registers a telephony callback to listen for call state changed to handle notification for
+ * hearing device during phone calls.
+ */
+ public void startListenForCallState() {
+ mTelephonyManager.registerTelephonyCallback(mCallbackExecutor, mTelephonyListener);
+ }
+
+ /**
+ * A telephony callback listener to listen to call state changes and show/dismiss notification
+ */
+ @VisibleForTesting
+ static class CallStateListener extends TelephonyCallback implements
+ TelephonyCallback.CallStateListener {
+
+ private static final String TAG =
+ "HearingDevice_CallStateListener";
+ private static final String ACTION_SWITCH_TO_BUILTIN_MIC =
+ "com.android.server.accessibility.hearingdevice.action.SWITCH_TO_BUILTIN_MIC";
+ private static final String ACTION_SWITCH_TO_HEARING_MIC =
+ "com.android.server.accessibility.hearingdevice.action.SWITCH_TO_HEARING_MIC";
+ private static final String ACTION_BLUETOOTH_DEVICE_DETAILS =
+ "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS";
+ private static final String KEY_BLUETOOTH_ADDRESS = "device_address";
+ private static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args";
+ private static final int MICROPHONE_SOURCE_VOICE_COMMUNICATION =
+ MediaRecorder.AudioSource.VOICE_COMMUNICATION;
+ private static final AudioDeviceAttributes BUILTIN_MIC = new AudioDeviceAttributes(
+ AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_BUILTIN_MIC, "");
+
+ private final Context mContext;
+ private NotificationManager mNotificationManager;
+ private AudioManager mAudioManager;
+ private BroadcastReceiver mHearingDeviceActionReceiver;
+ private BluetoothDevice mHearingDevice;
+ private boolean mIsNotificationShown = false;
+
+ CallStateListener(@NonNull Context context) {
+ mContext = context;
+ }
+
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onCallStateChanged(int state) {
+ // NotificationManagerService and AudioService are all initialized after
+ // AccessibilityManagerService.
+ // Can not get them in constructor. Need to get these services until callback is
+ // triggered.
+ mNotificationManager = mContext.getSystemService(NotificationManager.class);
+ mAudioManager = mContext.getSystemService(AudioManager.class);
+ if (mNotificationManager == null || mAudioManager == null) {
+ Log.w(TAG, "NotificationManager or AudioManager is not prepare yet.");
+ return;
+ }
+
+ if (state == TelephonyManager.CALL_STATE_IDLE) {
+ dismissNotificationIfNeeded();
+
+ if (mHearingDevice != null) {
+ // reset to its original status
+ setMicrophonePreferredForCalls(mHearingDevice.isMicrophonePreferredForCalls());
+ }
+ mHearingDevice = null;
+ }
+ if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
+ mHearingDevice = getSupportedInputHearingDeviceInfo(
+ mAudioManager.getAvailableCommunicationDevices());
+ if (mHearingDevice != null) {
+ showNotificationIfNeeded();
+ }
+ }
+ }
+
+ private void showNotificationIfNeeded() {
+ if (mIsNotificationShown) {
+ return;
+ }
+
+ showNotification(mHearingDevice.isMicrophonePreferredForCalls());
+ mIsNotificationShown = true;
+ }
+
+ private void dismissNotificationIfNeeded() {
+ if (!mIsNotificationShown) {
+ return;
+ }
+
+ dismissNotification();
+ mIsNotificationShown = false;
+ }
+
+ private void showNotification(boolean useRemoteMicrophone) {
+ mNotificationManager.notify(
+ SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH,
+ createSwitchInputNotification(useRemoteMicrophone));
+ registerReceiverIfNeeded();
+ }
+
+ private void dismissNotification() {
+ unregisterReceiverIfNeeded();
+ mNotificationManager.cancel(
+ SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH);
+ }
+
+ private BluetoothDevice getSupportedInputHearingDeviceInfo(List<AudioDeviceInfo> infoList) {
+ final BluetoothAdapter bluetoothAdapter = mContext.getSystemService(
+ BluetoothManager.class).getAdapter();
+ if (bluetoothAdapter == null) {
+ return null;
+ }
+ if (!isHapClientSupported()) {
+ return null;
+ }
+
+ final Set<String> inputDeviceAddress = Arrays.stream(
+ mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).map(
+ AudioDeviceInfo::getAddress).collect(Collectors.toSet());
+
+ //TODO: b/370812132 - Need to update if TYPE_LEA_HEARING_AID is added
+ final AudioDeviceInfo hearingDeviceInfo = infoList.stream()
+ .filter(info -> info.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET)
+ .filter(info -> inputDeviceAddress.contains(info.getAddress()))
+ .filter(info -> isHapClientDevice(bluetoothAdapter, info))
+ .findAny()
+ .orElse(null);
+
+ return (hearingDeviceInfo != null) ? bluetoothAdapter.getRemoteDevice(
+ hearingDeviceInfo.getAddress()) : null;
+ }
+
+ @VisibleForTesting
+ boolean isHapClientDevice(BluetoothAdapter bluetoothAdapter, AudioDeviceInfo info) {
+ BluetoothDevice device = bluetoothAdapter.getRemoteDevice(info.getAddress());
+ return ArrayUtils.contains(device.getUuids(), BluetoothUuid.HAS);
+ }
+
+ @VisibleForTesting
+ boolean isHapClientSupported() {
+ return BluetoothAdapter.getDefaultAdapter().getSupportedProfiles().contains(
+ BluetoothProfile.HAP_CLIENT);
+ }
+
+ private Notification createSwitchInputNotification(boolean useRemoteMicrophone) {
+ return new Notification.Builder(mContext,
+ SystemNotificationChannels.ACCESSIBILITY_HEARING_DEVICE)
+ .setContentTitle(getSwitchInputTitle(useRemoteMicrophone))
+ .setContentText(getSwitchInputMessage(useRemoteMicrophone))
+ .setSmallIcon(R.drawable.ic_settings_24dp)
+ .setColor(mContext.getResources().getColor(
+ com.android.internal.R.color.system_notification_accent_color))
+ .setLocalOnly(true)
+ .setCategory(Notification.CATEGORY_SYSTEM)
+ .setContentIntent(createPendingIntent(ACTION_BLUETOOTH_DEVICE_DETAILS))
+ .setActions(buildSwitchInputAction(useRemoteMicrophone),
+ buildOpenSettingsAction())
+ .build();
+ }
+
+ private Notification.Action buildSwitchInputAction(boolean useRemoteMicrophone) {
+ return useRemoteMicrophone
+ ? new Notification.Action.Builder(null,
+ mContext.getString(R.string.hearing_device_notification_switch_button),
+ createPendingIntent(ACTION_SWITCH_TO_BUILTIN_MIC)).build()
+ : new Notification.Action.Builder(null,
+ mContext.getString(R.string.hearing_device_notification_switch_button),
+ createPendingIntent(ACTION_SWITCH_TO_HEARING_MIC)).build();
+ }
+
+ private Notification.Action buildOpenSettingsAction() {
+ return new Notification.Action.Builder(null,
+ mContext.getString(R.string.hearing_device_notification_settings_button),
+ createPendingIntent(ACTION_BLUETOOTH_DEVICE_DETAILS)).build();
+ }
+
+ private PendingIntent createPendingIntent(String action) {
+ final Intent intent = new Intent(action);
+
+ switch (action) {
+ case ACTION_SWITCH_TO_BUILTIN_MIC, ACTION_SWITCH_TO_HEARING_MIC -> {
+ intent.setPackage(mContext.getPackageName());
+ return PendingIntent.getBroadcast(mContext, /* requestCode = */ 0, intent,
+ PendingIntent.FLAG_IMMUTABLE);
+ }
+ case ACTION_BLUETOOTH_DEVICE_DETAILS -> {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_BLUETOOTH_ADDRESS, mHearingDevice.getAddress());
+ intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
+ intent.addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ return PendingIntent.getActivity(mContext, /* requestCode = */ 0, intent,
+ PendingIntent.FLAG_IMMUTABLE);
+ }
+ }
+ return null;
+ }
+
+ private void setMicrophonePreferredForCalls(boolean useRemoteMicrophone) {
+ if (useRemoteMicrophone) {
+ switchToHearingMic();
+ } else {
+ switchToBuiltinMic();
+ }
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private void switchToBuiltinMic() {
+ mAudioManager.clearPreferredDevicesForCapturePreset(
+ MICROPHONE_SOURCE_VOICE_COMMUNICATION);
+ mAudioManager.setPreferredDeviceForCapturePreset(MICROPHONE_SOURCE_VOICE_COMMUNICATION,
+ BUILTIN_MIC);
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private void switchToHearingMic() {
+ // clear config to let audio manager to determine next priority device. We can assume
+ // user connects to hearing device here, so next priority device should be hearing
+ // device.
+ mAudioManager.clearPreferredDevicesForCapturePreset(
+ MICROPHONE_SOURCE_VOICE_COMMUNICATION);
+ }
+
+ private void registerReceiverIfNeeded() {
+ if (mHearingDeviceActionReceiver != null) {
+ return;
+ }
+ mHearingDeviceActionReceiver = new HearingDeviceActionReceiver();
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ACTION_SWITCH_TO_BUILTIN_MIC);
+ intentFilter.addAction(ACTION_SWITCH_TO_HEARING_MIC);
+ mContext.registerReceiver(mHearingDeviceActionReceiver, intentFilter,
+ Manifest.permission.MANAGE_ACCESSIBILITY, null, Context.RECEIVER_NOT_EXPORTED);
+ }
+
+ private void unregisterReceiverIfNeeded() {
+ if (mHearingDeviceActionReceiver == null) {
+ return;
+ }
+ mContext.unregisterReceiver(mHearingDeviceActionReceiver);
+ mHearingDeviceActionReceiver = null;
+ }
+
+ private CharSequence getSwitchInputTitle(boolean useRemoteMicrophone) {
+ return useRemoteMicrophone
+ ? mContext.getString(
+ R.string.hearing_device_switch_phone_mic_notification_title)
+ : mContext.getString(
+ R.string.hearing_device_switch_hearing_mic_notification_title);
+ }
+
+ private CharSequence getSwitchInputMessage(boolean useRemoteMicrophone) {
+ return useRemoteMicrophone
+ ? mContext.getString(
+ R.string.hearing_device_switch_phone_mic_notification_text)
+ : mContext.getString(
+ R.string.hearing_device_switch_hearing_mic_notification_text);
+ }
+
+ private class HearingDeviceActionReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (TextUtils.isEmpty(action)) {
+ return;
+ }
+
+ if (ACTION_SWITCH_TO_BUILTIN_MIC.equals(action)) {
+ switchToBuiltinMic();
+ showNotification(/* useRemoteMicrophone= */ false);
+ } else if (ACTION_SWITCH_TO_HEARING_MIC.equals(action)) {
+ switchToHearingMic();
+ showNotification(/* useRemoteMicrophone= */ true);
+ }
+ }
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
new file mode 100644
index 000000000000..efea21428937
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility;
+
+import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.app.Instrumentation;
+import android.app.NotificationManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioDevicePort;
+import android.media.AudioManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.messages.nano.SystemMessageProto;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Tests for the {@link HearingDevicePhoneCallNotificationController}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class HearingDevicePhoneCallNotificationControllerTest {
+ @Rule
+ public MockitoRule mockito = MockitoJUnit.rule();
+
+ private static final String TEST_ADDRESS = "55:66:77:88:99:AA";
+
+ private final Application mApplication = ApplicationProvider.getApplicationContext();
+ @Spy
+ private final Context mContext = mApplication.getApplicationContext();
+ private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
+ @Mock
+ private TelephonyManager mTelephonyManager;
+ @Mock
+ private NotificationManager mNotificationManager;
+ @Mock
+ private AudioManager mAudioManager;
+ private HearingDevicePhoneCallNotificationController mController;
+ private TestCallStateListener mTestCallStateListener;
+
+ @Before
+ public void setUp() {
+ mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
+ when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager);
+ when(mContext.getSystemService(NotificationManager.class)).thenReturn(mNotificationManager);
+ when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager);
+
+ mTestCallStateListener = new TestCallStateListener(mContext);
+ mController = new HearingDevicePhoneCallNotificationController(mContext,
+ mTestCallStateListener);
+ mController.startListenForCallState();
+ }
+
+ @Test
+ public void startListenForCallState_callbackNotNull() {
+ Mockito.reset(mTelephonyManager);
+ mController = new HearingDevicePhoneCallNotificationController(mContext);
+ ArgumentCaptor<TelephonyCallback> listenerCaptor = ArgumentCaptor.forClass(
+ TelephonyCallback.class);
+
+ mController.startListenForCallState();
+
+ verify(mTelephonyManager).registerTelephonyCallback(any(Executor.class),
+ listenerCaptor.capture());
+ TelephonyCallback callback = listenerCaptor.getValue();
+ assertThat(callback).isNotNull();
+ }
+
+ @Test
+ public void onCallStateChanged_stateOffHook_hapDevice_showNotification() {
+ AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS,
+ AudioManager.DEVICE_OUT_BLE_HEADSET);
+ when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(
+ new AudioDeviceInfo[]{hapDeviceInfo});
+ when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo));
+
+ mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK);
+
+ verify(mNotificationManager).notify(
+ eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any());
+ }
+
+ @Test
+ public void onCallStateChanged_stateOffHook_a2dpDevice_noNotification() {
+ AudioDeviceInfo a2dpDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS,
+ AudioManager.DEVICE_OUT_BLUETOOTH_A2DP);
+ when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(
+ new AudioDeviceInfo[]{a2dpDeviceInfo});
+ when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(a2dpDeviceInfo));
+
+ mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK);
+
+ verify(mNotificationManager, never()).notify(
+ eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any());
+ }
+
+ @Test
+ public void onCallStateChanged_stateOffHookThenIdle_hapDeviceInfo_cancelNotification() {
+ AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS,
+ AudioManager.DEVICE_OUT_BLE_HEADSET);
+ when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(
+ new AudioDeviceInfo[]{hapDeviceInfo});
+ when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo));
+
+ mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK);
+ mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_IDLE);
+
+ verify(mNotificationManager).cancel(
+ eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH));
+ }
+
+ private AudioDeviceInfo createAudioDeviceInfo(String address, int type) {
+ AudioDevicePort audioDevicePort = mock(AudioDevicePort.class);
+ doReturn(type).when(audioDevicePort).type();
+ doReturn(address).when(audioDevicePort).address();
+ doReturn("testDevice").when(audioDevicePort).name();
+
+ return new AudioDeviceInfo(audioDevicePort);
+ }
+
+ /**
+ * For easier testing for CallStateListener, override methods that contain final object.
+ */
+ private static class TestCallStateListener extends
+ HearingDevicePhoneCallNotificationController.CallStateListener {
+
+ TestCallStateListener(@NonNull Context context) {
+ super(context);
+ }
+
+ @Override
+ boolean isHapClientSupported() {
+ return true;
+ }
+
+ @Override
+ boolean isHapClientDevice(BluetoothAdapter bluetoothAdapter, AudioDeviceInfo info) {
+ return TEST_ADDRESS.equals(info.getAddress());
+ }
+ }
+}