Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9e454698 authored by Jasmine Cha's avatar Jasmine Cha
Browse files

Show the output switcher when no media is playing



- Support output switcher for system routing.
- Add an new string to indicate the device that
  audio will output to.

Bug: 284227163
Test: device/host atest
 atest MediaOutputPreferenceControllerTest
 atest AudioOutputSwitchPreferenceControllerTest
 atest MediaOutputIndicatorSliceTest

Change-Id: I94bcf84e7e93b3e4f5db1d95d5380a54a3e0c460
Signed-off-by: default avatarJasmine Cha <chajasmine@google.com>
parent 13404277
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -10927,6 +10927,9 @@
    <!-- Title with application label for media output settings. [CHAR LIMIT=NONE] -->
    <string name="media_output_label_title">Play <xliff:g id="label" example="Music Player">%s</xliff:g> on</string>
    <!-- Title for media output settings without media is playing -->
    <string name="media_output_title_without_playing">Audio will play on</string>
    <!-- Summary for media output default settings. (this device) [CHAR LIMIT=30] -->
    <string name="media_output_default_summary">This device</string>
+25 −11
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.settings.media;

import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI;
import static com.android.settingslib.media.flags.Flags.enableOutputSwitcherForSystemRouting;

import android.annotation.ColorInt;
import android.content.Context;
@@ -58,7 +59,12 @@ public class MediaOutputIndicatorSlice implements CustomSliceable {
        }
        final IconCompat icon = IconCompat.createWithResource(mContext,
                com.android.internal.R.drawable.ic_settings_bluetooth);
        final CharSequence title = mContext.getString(R.string.media_output_label_title,
        final int stringRes = enableOutputSwitcherForSystemRouting()
                ? (getWorker().getActiveLocalMediaController() != null
                        ? R.string.media_output_label_title
                        : R.string.media_output_title_without_playing)
                : R.string.media_output_label_title;
        final CharSequence title = mContext.getString(stringRes,
                Utils.getApplicationLabel(mContext, getWorker().getPackageName()));
        final SliceAction primarySliceAction = SliceAction.create(
                getBroadcastIntent(mContext), icon, ListBuilder.ICON_IMAGE, title);
@@ -117,21 +123,24 @@ public class MediaOutputIndicatorSlice implements CustomSliceable {
        // 2. worker is not null
        // 3. Available devices are more than 0
        // 4. The local media session is active and the state is playing.
        //    - if !enableOutputSwitcherForSystemRouting(), (4) will be bypass.
        return getWorker() != null
                && !com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)
                && getWorker().getMediaDevices().size() > 0
                && getWorker().getActiveLocalMediaController() != null;
                && (enableOutputSwitcherForSystemRouting()
                        ? true : getWorker().getActiveLocalMediaController() != null);
    }

    @Override
    public void onNotifyChange(Intent intent) {
        final MediaController mediaController = getWorker().getActiveLocalMediaController();

        if (mediaController == null) {
            Log.d(TAG, "No active local media controller");
            return;
        }
        // Launch media output dialog
        if (enableOutputSwitcherForSystemRouting() && mediaController == null) {
            mContext.sendBroadcast(new Intent()
                    .setPackage(MediaOutputConstants.SYSTEMUI_PACKAGE_NAME)
                    .setAction(MediaOutputConstants.ACTION_LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG));
        } else if (mediaController != null) {
            mContext.sendBroadcast(new Intent()
                    .setPackage(MediaOutputConstants.SYSTEMUI_PACKAGE_NAME)
                    .setAction(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG)
@@ -139,6 +148,11 @@ public class MediaOutputIndicatorSlice implements CustomSliceable {
                            mediaController.getSessionToken())
                    .putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME,
                            mediaController.getPackageName()));
        } else {
            Log.d(TAG, "No active local media controller");
            return;
        }

        // Dismiss volume panel
        mContext.sendBroadcast(new Intent()
                .setPackage(MediaOutputConstants.SETTINGS_PACKAGE_NAME)
+38 −1
Original line number Diff line number Diff line
@@ -18,6 +18,9 @@ package com.android.settings.sound;

import static android.media.AudioManager.STREAM_DEVICES_CHANGED_ACTION;

import static com.android.settingslib.media.flags.Flags.enableOutputSwitcherForSystemRouting;

import android.annotation.Nullable;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -28,6 +31,8 @@ import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.MediaRouter;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.os.Handler;
import android.os.Looper;
import android.util.FeatureFlagUtils;
@@ -79,6 +84,8 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont
    private final WiredHeadsetBroadcastReceiver mReceiver;
    private final Handler mHandler;
    private LocalBluetoothManager mLocalBluetoothManager;
    @Nullable private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener;
    @Nullable private MediaSessionManager mMediaSessionManager;

    public interface AudioSwitchCallback {
        void onPreferenceDataChanged(ListPreference preference);
@@ -107,6 +114,14 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont
            return;
        }
        mProfileManager = mLocalBluetoothManager.getProfileManager();

        if (enableOutputSwitcherForSystemRouting()) {
            mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
            mSessionListener = new SessionChangeListener();
        } else {
            mMediaSessionManager = null;
            mSessionListener = null;
        }
    }

    /**
@@ -329,13 +344,27 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont
        // Register for misc other intent broadcasts.
        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
        intentFilter.addAction(STREAM_DEVICES_CHANGED_ACTION);

        if (enableOutputSwitcherForSystemRouting()) {
            mContext.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED);
            if (mMediaSessionManager != null) {
                mMediaSessionManager.addOnActiveSessionsChangedListener(
                        mSessionListener, null, mHandler);
            }
        } else {
            mContext.registerReceiver(mReceiver, intentFilter);
        }
    }

    private void unregister() {
        mLocalBluetoothManager.getEventManager().unregisterCallback(this);
        mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback);
        mContext.unregisterReceiver(mReceiver);
        if (enableOutputSwitcherForSystemRouting()) {
            if (mMediaSessionManager != null) {
                mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionListener);
            }
        }
    }

    /** Notifications of audio device connection and disconnection events. */
@@ -362,4 +391,12 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont
            }
        }
    }

    private class SessionChangeListener
            implements MediaSessionManager.OnActiveSessionsChangedListener {
        @Override
        public void onActiveSessionsChanged(List<MediaController> controllers) {
            updateState(mPreference);
        }
    }
}
+39 −19
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package com.android.settings.sound;

import static com.android.settingslib.media.flags.Flags.enableOutputSwitcherForSystemRouting;

import android.annotation.Nullable;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
@@ -46,21 +49,22 @@ import java.util.List;
 */
public class MediaOutputPreferenceController extends AudioSwitchPreferenceController {

    private MediaController mMediaController;
    private static final String TAG = "MediaOutputPreferenceController";
    @Nullable private MediaController mMediaController;
    private MediaSessionManager mMediaSessionManager;

    public MediaOutputPreferenceController(Context context, String key) {
        super(context, key);
        mMediaController = MediaOutputUtils.getActiveLocalMediaController(context.getSystemService(
                MediaSessionManager.class));
        mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
        mMediaController = MediaOutputUtils.getActiveLocalMediaController(mMediaSessionManager);
    }

    @Override
    public void displayPreference(PreferenceScreen screen) {
        super.displayPreference(screen);

        if (!Utils.isAudioModeOngoingCall(mContext) && mMediaController != null) {
            mPreference.setVisible(true);
        }
        mPreference.setVisible(!Utils.isAudioModeOngoingCall(mContext)
                && (enableOutputSwitcherForSystemRouting() ? true : mMediaController != null));
    }

    @Override
@@ -70,10 +74,15 @@ public class MediaOutputPreferenceController extends AudioSwitchPreferenceContro
            return;
        }

        if (enableOutputSwitcherForSystemRouting()) {
            mMediaController = MediaOutputUtils.getActiveLocalMediaController(mMediaSessionManager);
        } else {
            if (mMediaController == null) {
                // No active local playback
                return;
            }
        }


        if (Utils.isAudioModeOngoingCall(mContext)) {
            // Ongoing call status, switch entry for media will be disabled.
@@ -95,9 +104,14 @@ public class MediaOutputPreferenceController extends AudioSwitchPreferenceContro
                || (connectedLeAudioDevices != null && !connectedLeAudioDevices.isEmpty()))) {
            activeDevice = findActiveDevice();
        }

        if (mMediaController == null) {
            mPreference.setTitle(mContext.getString(R.string.media_output_title_without_playing));
        } else {
            mPreference.setTitle(mContext.getString(R.string.media_output_label_title,
                    com.android.settings.Utils.getApplicationLabel(mContext,
                    mMediaController.getPackageName())));
        }
        mPreference.setSummary((activeDevice == null) ?
                mContext.getText(R.string.media_output_default_summary) :
                activeDevice.getAlias());
@@ -145,6 +159,11 @@ public class MediaOutputPreferenceController extends AudioSwitchPreferenceContro
    @Override
    public boolean handlePreferenceTreeClick(Preference preference) {
        if (TextUtils.equals(preference.getKey(), getPreferenceKey())) {
            if (enableOutputSwitcherForSystemRouting() && mMediaController == null) {
                mContext.sendBroadcast(new Intent()
                        .setAction(MediaOutputConstants.ACTION_LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG)
                        .setPackage(MediaOutputConstants.SYSTEMUI_PACKAGE_NAME));
            } else if (mMediaController != null) {
                mContext.sendBroadcast(new Intent()
                        .setAction(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG)
                        .setPackage(MediaOutputConstants.SYSTEMUI_PACKAGE_NAME)
@@ -152,6 +171,7 @@ public class MediaOutputPreferenceController extends AudioSwitchPreferenceContro
                                mMediaController.getPackageName())
                        .putExtra(MediaOutputConstants.KEY_MEDIA_SESSION_TOKEN,
                                mMediaController.getSessionToken()));
            }
            return true;
        }
        return false;
+48 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@
package com.android.settings.media;

import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI;
import static com.android.settingslib.media.flags.Flags.FLAG_ENABLE_OUTPUT_SWITCHER_FOR_SYSTEM_ROUTING;

import static com.google.common.truth.Truth.assertThat;

@@ -38,6 +39,7 @@ import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.net.Uri;
import android.os.Process;
import android.platform.test.flag.junit.SetFlagsRule;
import android.text.TextUtils;

import androidx.slice.Slice;
@@ -55,6 +57,7 @@ import com.android.settingslib.media.MediaOutputConstants;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -96,6 +99,9 @@ public class MediaOutputIndicatorSliceTest {
    @Mock
    private Drawable mTestDrawable;

    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    private Context mContext;
    private MediaOutputIndicatorSlice mMediaOutputIndicatorSlice;
    private AudioManager mAudioManager;
@@ -254,6 +260,34 @@ public class MediaOutputIndicatorSliceTest {
                MediaOutputConstants.KEY_MEDIA_SESSION_TOKEN) == null).isTrue();
    }

    @Test
    public void onNotifyChange_withoutMediaControllerFlagEnabled_verifyIntentExtra() {
        mSetFlagsRule.enableFlags(FLAG_ENABLE_OUTPUT_SWITCHER_FOR_SYSTEM_ROUTING);
        doReturn(null).when(sMediaOutputIndicatorWorker)
                .getActiveLocalMediaController();
        ArgumentCaptor<Intent> argument = ArgumentCaptor.forClass(Intent.class);

        mMediaOutputIndicatorSlice.onNotifyChange(null);
        verify(mContext, times(2)).sendBroadcast(argument.capture());
        List<Intent> intentList = argument.getAllValues();
        Intent intent = intentList.get(0);

        assertThat(intent.getAction()).isEqualTo(
                MediaOutputConstants.ACTION_LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG);
        assertThat(TextUtils.equals(MediaOutputConstants.SYSTEMUI_PACKAGE_NAME,
                intent.getPackage())).isTrue();
    }

    @Test
    public void onNotifyChange_withoutMediaControllerFlagDisabled_doNothing() {
        mSetFlagsRule.disableFlags(FLAG_ENABLE_OUTPUT_SWITCHER_FOR_SYSTEM_ROUTING);
        doReturn(null).when(sMediaOutputIndicatorWorker)
                .getActiveLocalMediaController();

        mMediaOutputIndicatorSlice.onNotifyChange(null);
    }


    @Test
    public void isVisible_allConditionMatched_returnTrue() {
        mAudioManager.setMode(AudioManager.MODE_NORMAL);
@@ -268,6 +302,7 @@ public class MediaOutputIndicatorSliceTest {

    @Test
    public void isVisible_noActiveSession_returnFalse() {
        mSetFlagsRule.disableFlags(FLAG_ENABLE_OUTPUT_SWITCHER_FOR_SYSTEM_ROUTING);
        mAudioManager.setMode(AudioManager.MODE_NORMAL);
        mDevices.add(mDevice1);

@@ -278,6 +313,19 @@ public class MediaOutputIndicatorSliceTest {
        assertThat(mMediaOutputIndicatorSlice.isVisible()).isFalse();
    }

    @Test
    public void isVisible_noActiveSession_returnTrue() {
        mSetFlagsRule.enableFlags(FLAG_ENABLE_OUTPUT_SWITCHER_FOR_SYSTEM_ROUTING);
        mAudioManager.setMode(AudioManager.MODE_NORMAL);
        mDevices.add(mDevice1);

        when(sMediaOutputIndicatorWorker.getMediaDevices()).thenReturn(mDevices);
        doReturn(mMediaController).when(sMediaOutputIndicatorWorker)
                .getActiveLocalMediaController();

        assertThat(mMediaOutputIndicatorSlice.isVisible()).isTrue();
    }

    private void initPackage() {
        mShadowPackageManager = Shadows.shadowOf(mContext.getPackageManager());
        mAppInfo = new ApplicationInfo();
Loading