From 6dc3e5938fb3a7e5677728f8914b65f5f96209fd Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Mon, 17 Feb 2025 13:47:49 +0800 Subject: [PATCH] [Audiosharing] Show add source notif when app not in foreground Test: atest Bug: 395786392 Flag: com.android.settingslib.flags.enable_le_audio_sharing Change-Id: I3c88c4209cf34c8f337965ffb45fb8e9a4db0e05 --- res/values/strings.xml | 2 + .../audiosharing/AudioSharingReceiver.java | 163 ++++++++++++++++-- .../AudioSharingReceiverTest.java | 102 +++++++++-- 3 files changed, 236 insertions(+), 31 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 07d93fc9d7b..850f8eda271 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -14035,6 +14035,8 @@ To pair a new device, turn off Audio Sharing first. Turn off + + Share audio with %1$s Connect to a LE audio stream diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiver.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiver.java index 740fb359afc..43f6a5eaf82 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiver.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiver.java @@ -16,17 +16,25 @@ package com.android.settings.connecteddevice.audiosharing; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE; + +import android.Manifest; +import android.app.ActivityManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.os.Bundle; +import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import com.android.settings.R; @@ -43,12 +51,19 @@ public class AudioSharingReceiver extends BroadcastReceiver { "com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS"; private static final String ACTION_LE_AUDIO_SHARING_STOP = "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STOP"; + private static final String ACTION_LE_AUDIO_SHARING_ADD_SOURCE = + "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_ADD_SOURCE"; + private static final String ACTION_LE_AUDIO_SHARING_CANCEL_NOTIF = + "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_CANCEL_NOTIF"; + private static final String EXTRA_NOTIF_ID = "NOTIF_ID"; private static final String CHANNEL_ID = "bluetooth_notification_channel"; - private static final int NOTIFICATION_ID = + private static final int AUDIO_SHARING_NOTIFICATION_ID = com.android.settingslib.R.drawable.ic_bt_le_audio_sharing; + private static final int ADD_SOURCE_NOTIFICATION_ID = R.string.share_audio_notification_title; + private static final int NOTIF_AUTO_DISMISS_MILLIS = 300000; //5mins @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(@NonNull Context context, @NonNull Intent intent) { String action = intent.getAction(); if (action == null) { Log.w(TAG, "Received unexpected intent with null action."); @@ -74,10 +89,12 @@ public class AudioSharingReceiver extends BroadcastReceiver { // isLeAudioBroadcastSourceSupported() and BluetoothAdapter# // isLeAudioBroadcastAssistantSupported() always return FEATURE_SUPPORTED // or FEATURE_NOT_SUPPORTED when BT and BLE off - cancelSharingNotification(context); + cancelSharingNotification(context, AUDIO_SHARING_NOTIFICATION_ID); metricsFeatureProvider.action( context, SettingsEnums.ACTION_CANCEL_AUDIO_SHARING_NOTIFICATION, LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_STATE_CHANGE); + cancelSharingNotification(context, ADD_SOURCE_NOTIFICATION_ID); + // TODO: add metric } else { Log.w( TAG, @@ -99,27 +116,40 @@ public class AudioSharingReceiver extends BroadcastReceiver { // isLeAudioBroadcastSourceSupported() and BluetoothAdapter# // isLeAudioBroadcastAssistantSupported() always return FEATURE_SUPPORTED // or FEATURE_NOT_SUPPORTED when BT and BLE off - cancelSharingNotification(context); + cancelSharingNotification(context, AUDIO_SHARING_NOTIFICATION_ID); metricsFeatureProvider.action( context, SettingsEnums.ACTION_CANCEL_AUDIO_SHARING_NOTIFICATION, ACTION_LE_AUDIO_SHARING_STOP); + cancelSharingNotification(context, ADD_SOURCE_NOTIFICATION_ID); + break; + case LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED: + if (!BluetoothUtils.isAudioSharingUIAvailable(context)) { + Log.d(TAG, "Skip ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED, feature disabled."); + return; + } + BluetoothDevice device = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE, + BluetoothDevice.class); + if (device == null) { + Log.d(TAG, "Skip ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED, null device"); + return; + } + if (isAppInForeground(context)) { + // TODO: show dialog + Log.d(TAG, "App in foreground, show share audio dialog"); + } else { + Log.d(TAG, "App not in foreground, show share audio notification"); + showAddSourceNotification(context, device); + } break; default: Log.w(TAG, "Received unexpected intent " + intent.getAction()); } } - private void showSharingNotification(Context context) { + private void showSharingNotification(@NonNull Context context) { NotificationManager nm = context.getSystemService(NotificationManager.class); - if (nm.getNotificationChannel(CHANNEL_ID) == null) { - Log.d(TAG, "Create bluetooth notification channel"); - NotificationChannel notificationChannel = - new NotificationChannel( - CHANNEL_ID, - context.getString(com.android.settings.R.string.bluetooth), - NotificationManager.IMPORTANCE_HIGH); - nm.createNotificationChannel(notificationChannel); - } + if (nm == null) return; + createNotificationChannelIfNeeded(nm, context); Intent stopIntent = new Intent(ACTION_LE_AUDIO_SHARING_STOP).setPackage(context.getPackageName()); PendingIntent stopPendingIntent = @@ -174,11 +204,110 @@ public class AudioSharingReceiver extends BroadcastReceiver { .addAction(stopAction) .addAction(settingsAction) .addExtras(extras); - nm.notify(NOTIFICATION_ID, builder.build()); + nm.notify(AUDIO_SHARING_NOTIFICATION_ID, builder.build()); } - private void cancelSharingNotification(Context context) { + private void showAddSourceNotification(@NonNull Context context, + @NonNull BluetoothDevice device) { NotificationManager nm = context.getSystemService(NotificationManager.class); - nm.cancel(NOTIFICATION_ID); + if (nm == null) return; + createNotificationChannelIfNeeded(nm, context); + Intent addSourceIntent = + new Intent(ACTION_LE_AUDIO_SHARING_ADD_SOURCE).setPackage(context.getPackageName()) + .putExtra(EXTRA_BLUETOOTH_DEVICE, device); + PendingIntent addSourcePendingIntent = + PendingIntent.getBroadcast( + context, + R.string.audio_sharing_share_button_label, + addSourceIntent, + PendingIntent.FLAG_IMMUTABLE); + NotificationCompat.Action addSourceAction = + new NotificationCompat.Action.Builder( + 0, + context.getString(R.string.audio_sharing_share_button_label), + addSourcePendingIntent) + .build(); + Intent cancelIntent = new Intent(ACTION_LE_AUDIO_SHARING_CANCEL_NOTIF).setPackage( + context.getPackageName()) + .putExtra(EXTRA_NOTIF_ID, ADD_SOURCE_NOTIFICATION_ID); + PendingIntent cancelPendingIntent = + PendingIntent.getBroadcast( + context, + R.string.cancel, + cancelIntent, + PendingIntent.FLAG_IMMUTABLE); + NotificationCompat.Action cancelAction = + new NotificationCompat.Action.Builder( + 0, + context.getString(R.string.cancel), + cancelPendingIntent) + .build(); + final Bundle extras = new Bundle(); + extras.putString( + Notification.EXTRA_SUBSTITUTE_APP_NAME, + context.getString(R.string.audio_sharing_title)); + String deviceName = device.getAlias(); + if (TextUtils.isEmpty(deviceName)) { + deviceName = device.getAddress(); + } + NotificationCompat.Builder builder = + new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing) + .setLocalOnly(true) + .setContentTitle(context.getString(R.string.share_audio_notification_title, + deviceName)) + .setContentText( + context.getString(R.string.audio_sharing_notification_content)) + .setOngoing(true) + .setSilent(true) + .setColor( + context.getColor( + com.android.internal.R.color + .system_notification_accent_color)) + .addAction(addSourceAction) + .addAction(cancelAction) + .setTimeoutAfter(NOTIF_AUTO_DISMISS_MILLIS) + .addExtras(extras); + nm.notify(ADD_SOURCE_NOTIFICATION_ID, builder.build()); + } + + private void cancelSharingNotification(@NonNull Context context, int notifId) { + NotificationManager nm = context.getSystemService(NotificationManager.class); + if (nm != null) { + nm.cancel(notifId); + } + } + + private void createNotificationChannelIfNeeded(@NonNull NotificationManager nm, + @NonNull Context context) { + if (nm.getNotificationChannel(CHANNEL_ID) == null) { + Log.d(TAG, "Create bluetooth notification channel"); + NotificationChannel notificationChannel = + new NotificationChannel( + CHANNEL_ID, + context.getString(com.android.settings.R.string.bluetooth), + NotificationManager.IMPORTANCE_HIGH); + nm.createNotificationChannel(notificationChannel); + } + } + + private boolean isAppInForeground(@NonNull Context context) { + try { + ActivityManager activityManager = context.getSystemService(ActivityManager.class); + String packageName = context.getPackageName(); + if (context.getPackageManager().checkPermission(Manifest.permission.PACKAGE_USAGE_STATS, + packageName) != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "check isAppInForeground, returns false due to no permission"); + return false; + } + if (packageName != null && activityManager.getPackageImportance(packageName) + == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + Log.d(TAG, "check isAppInForeground, returns true"); + return true; + } + } catch (RuntimeException e) { + Log.d(TAG, "check isAppInForeground, error = " + e.getMessage()); + } + return false; } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiverTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiverTest.java index e72003818bd..9bda070dc40 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiverTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiverTest.java @@ -16,28 +16,38 @@ package com.android.settings.connecteddevice.audiosharing; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_STATE_CHANGE; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.BROADCAST_STATE_OFF; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.BROADCAST_STATE_ON; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_LE_AUDIO_SHARING_STATE; 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.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import android.Manifest; +import android.app.ActivityManager; import android.app.Notification; import android.app.NotificationManager; import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothStatusCodes; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import com.android.settings.bluetooth.Utils; @@ -72,6 +82,7 @@ import java.util.stream.Collectors; public class AudioSharingReceiverTest { private static final String ACTION_LE_AUDIO_SHARING_STOP = "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STOP"; + private static final String TEST_DEVICE_NAME = "test"; @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -88,7 +99,7 @@ public class AudioSharingReceiverTest { @Before public void setUp() { - mContext = RuntimeEnvironment.getApplication(); + mContext = spy(RuntimeEnvironment.getApplication()); mShadowApplication = Shadow.extract(mContext); mShadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm); mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); @@ -130,9 +141,8 @@ public class AudioSharingReceiverTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void broadcastReceiver_receiveAudioSharingStateOn_flagOff_doNothing() { - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE); intent.setPackage(mContext.getPackageName()); intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, BROADCAST_STATE_ON); @@ -144,8 +154,8 @@ public class AudioSharingReceiverTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void broadcastReceiver_receiveAudioSharingStateOn_broadcastDisabled_doNothing() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED); @@ -160,9 +170,8 @@ public class AudioSharingReceiverTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void broadcastReceiver_receiveAudioSharingStateChangeIntentNoState_doNothing() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE); intent.setPackage(mContext.getPackageName()); AudioSharingReceiver audioSharingReceiver = getAudioSharingReceiver(intent); @@ -173,9 +182,8 @@ public class AudioSharingReceiverTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void broadcastReceiver_receiveAudioSharingStateOn_broadcastEnabled_showNotification() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE); intent.setPackage(mContext.getPackageName()); intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, BROADCAST_STATE_ON); @@ -188,9 +196,9 @@ public class AudioSharingReceiverTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void broadcastReceiver_receiveAudioSharingStateOff_broadcastDisabled_cancelNotification() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED); @@ -207,10 +215,9 @@ public class AudioSharingReceiverTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void broadcastReceiver_receiveAudioSharingStateOff_broadcastEnabled_cancelNotification() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE); intent.setPackage(mContext.getPackageName()); intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, BROADCAST_STATE_OFF); @@ -224,8 +231,8 @@ public class AudioSharingReceiverTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void broadcastReceiver_receiveAudioSharingStop_broadcastDisabled_cancelNotification() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED); @@ -242,8 +249,8 @@ public class AudioSharingReceiverTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void broadcastReceiver_receiveAudioSharingStop_notInBroadcast_cancelNotification() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(false); int broadcastId = 1; when(mBroadcast.getLatestBroadcastId()).thenReturn(broadcastId); @@ -261,8 +268,8 @@ public class AudioSharingReceiverTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void broadcastReceiver_receiveAudioSharingStop_inBroadcast_stopBroadcast() { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); when(mBroadcast.isEnabled(null)).thenReturn(true); int broadcastId = 1; when(mBroadcast.getLatestBroadcastId()).thenReturn(broadcastId); @@ -281,6 +288,61 @@ public class AudioSharingReceiverTest { ACTION_LE_AUDIO_SHARING_STOP); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void broadcastReceiver_receiveAudioSharingDeviceConnected_broadcastDisabled_doNothing() { + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED); + + BluetoothDevice device = mock(BluetoothDevice.class); + when(device.getAlias()).thenReturn(TEST_DEVICE_NAME); + setAppInForeground(false); + Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED); + intent.setPackage(mContext.getPackageName()); + intent.putExtra(EXTRA_BLUETOOTH_DEVICE, device); + AudioSharingReceiver audioSharingReceiver = getAudioSharingReceiver(intent); + audioSharingReceiver.onReceive(mContext, intent); + + verify(mNm, never()).notify( + eq(com.android.settings.R.string.share_audio_notification_title), + any(Notification.class)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void broadcastReceiver_receiveAudioSharingDeviceConnected_showDialog() { + BluetoothDevice device = mock(BluetoothDevice.class); + when(device.getAlias()).thenReturn(TEST_DEVICE_NAME); + setAppInForeground(true); + Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED); + intent.setPackage(mContext.getPackageName()); + intent.putExtra(EXTRA_BLUETOOTH_DEVICE, device); + AudioSharingReceiver audioSharingReceiver = getAudioSharingReceiver(intent); + audioSharingReceiver.onReceive(mContext, intent); + + verify(mNm, never()).notify( + eq(com.android.settings.R.string.share_audio_notification_title), + any(Notification.class)); + // TODO: verify show dialog once impl complete + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void broadcastReceiver_receiveAudioSharingDeviceConnected_showNotification() { + BluetoothDevice device = mock(BluetoothDevice.class); + when(device.getAlias()).thenReturn(TEST_DEVICE_NAME); + setAppInForeground(false); + Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED); + intent.setPackage(mContext.getPackageName()); + intent.putExtra(EXTRA_BLUETOOTH_DEVICE, device); + AudioSharingReceiver audioSharingReceiver = getAudioSharingReceiver(intent); + audioSharingReceiver.onReceive(mContext, intent); + + // TODO: verify no dialog once impl complete + verify(mNm).notify(eq(com.android.settings.R.string.share_audio_notification_title), + any(Notification.class)); + } + private AudioSharingReceiver getAudioSharingReceiver(Intent intent) { assertThat(mShadowApplication.hasReceiverForIntent(intent)).isTrue(); List receiversForIntent = @@ -290,4 +352,16 @@ public class AudioSharingReceiverTest { assertThat(broadcastReceiver).isInstanceOf(AudioSharingReceiver.class); return (AudioSharingReceiver) broadcastReceiver; } + + private void setAppInForeground(boolean foreground) { + ActivityManager activityManager = mock(ActivityManager.class); + when(mContext.getSystemService(ActivityManager.class)).thenReturn(activityManager); + when(activityManager.getPackageImportance(mContext.getPackageName())).thenReturn( + foreground ? ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + : ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE); + PackageManager packageManager = mock(PackageManager.class); + when(mContext.getPackageManager()).thenReturn(packageManager); + when(packageManager.checkPermission(Manifest.permission.PACKAGE_USAGE_STATS, + mContext.getPackageName())).thenReturn(PackageManager.PERMISSION_GRANTED); + } }