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

Commit b6c8fab8 authored by tim peng's avatar tim peng Committed by Android (Google) Code Review
Browse files

Merge "Launch output switcher with media package information" into rvc-dev

parents e28bd9d6 14d0d125
Loading
Loading
Loading
Loading
+34 −12
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.session.MediaController;
import android.net.Uri;
import android.util.Log;

@@ -36,6 +37,8 @@ import com.android.internal.util.CollectionUtils;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.slices.CustomSliceable;
import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settings.slices.SliceBroadcastReceiver;
import com.android.settingslib.bluetooth.A2dpProfile;
import com.android.settingslib.bluetooth.HearingAidProfile;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
@@ -52,6 +55,7 @@ public class MediaOutputIndicatorSlice implements CustomSliceable {
    private Context mContext;
    private LocalBluetoothManager mLocalBluetoothManager;
    private LocalBluetoothProfileManager mProfileManager;
    private MediaOutputIndicatorWorker mWorker;

    public MediaOutputIndicatorSlice(Context context) {
        mContext = context;
@@ -66,22 +70,18 @@ public class MediaOutputIndicatorSlice implements CustomSliceable {
    @Override
    public Slice getSlice() {
        if (!isVisible()) {
            return new ListBuilder(mContext, MEDIA_OUTPUT_INDICATOR_SLICE_URI, ListBuilder.INFINITY)
            return new ListBuilder(mContext, getUri(), ListBuilder.INFINITY)
                    .setIsError(true)
                    .build();
        }
        final IconCompat icon = IconCompat.createWithResource(mContext,
                com.android.internal.R.drawable.ic_settings_bluetooth);
        final CharSequence title = mContext.getText(R.string.media_output_title);
        final PendingIntent primaryActionIntent = PendingIntent.getActivity(mContext,
                0 /* requestCode */, getMediaOutputSliceIntent(), 0 /* flags */);
        final SliceAction primarySliceAction = SliceAction.createDeeplink(
                primaryActionIntent, icon, ListBuilder.ICON_IMAGE, title);
                getBroadcastIntent(), icon, ListBuilder.ICON_IMAGE, title);
        @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext);
        // To set an empty icon to indent the row
        final ListBuilder listBuilder = new ListBuilder(mContext,
                MEDIA_OUTPUT_INDICATOR_SLICE_URI,
                ListBuilder.INFINITY)
        final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY)
                .setAccentColor(color)
                .addRow(new ListBuilder.RowBuilder()
                        .setTitle(title)
@@ -96,11 +96,11 @@ public class MediaOutputIndicatorSlice implements CustomSliceable {
        return IconCompat.createWithBitmap(bitmap);
    }

    private Intent getMediaOutputSliceIntent() {
        final Intent intent = new Intent()
                .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        return intent;
    private PendingIntent getBroadcastIntent() {
        final Intent intent = new Intent(getUri().toString());
        intent.setClass(mContext, SliceBroadcastReceiver.class);
        return PendingIntent.getBroadcast(mContext, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
    }

    @Override
@@ -120,6 +120,28 @@ public class MediaOutputIndicatorSlice implements CustomSliceable {
        return MediaOutputIndicatorWorker.class;
    }

    @Override
    public void onNotifyChange(Intent i) {
        final MediaController mediaController = getWorker().getActiveLocalMediaController();
        final Intent intent = new Intent()
                .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (mediaController != null) {
            intent.putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN,
                    mediaController.getSessionToken());
            intent.putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
                    mediaController.getPackageName());
        }
        mContext.startActivity(intent);
    }

    private MediaOutputIndicatorWorker getWorker() {
        if (mWorker == null) {
            mWorker = SliceBackgroundWorker.getInstance(getUri());
        }
        return mWorker;
    }

    private boolean isVisible() {
        // To decide Slice's visibility.
        // Return true if
+26 −2
Original line number Diff line number Diff line
@@ -24,18 +24,21 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.Nullable;

import com.android.settings.bluetooth.Utils;
import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;

import java.io.IOException;

/**
 * Listener for background change from {@code BluetoothCallback} to update media output indicator.
 */
@@ -100,6 +103,27 @@ public class MediaOutputIndicatorWorker extends SliceBackgroundWorker implements
        notifySliceChange();
    }

    @Nullable
    MediaController getActiveLocalMediaController() {
        final MediaSessionManager mMediaSessionManager = mContext.getSystemService(
                MediaSessionManager.class);

        for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) {
            final MediaController.PlaybackInfo pi = controller.getPlaybackInfo();
            if (pi == null) {
                return null;
            }
            final PlaybackState playbackState = controller.getPlaybackState();
            if (playbackState == null) {
                return null;
            }
            if (pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
                    && playbackState.getState() == PlaybackState.STATE_PLAYING) {
                return controller;
            }
        }
        return null;
    }
    private class DevicesChangedBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
+66 −2
Original line number Diff line number Diff line
@@ -17,16 +17,25 @@

package com.android.settings.media;

import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI;

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

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.net.Uri;
import android.text.TextUtils;

import androidx.slice.Slice;
import androidx.slice.SliceMetadata;
@@ -34,33 +43,42 @@ import androidx.slice.SliceProvider;
import androidx.slice.widget.SliceLiveData;

import com.android.settings.R;
import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
import com.android.settingslib.bluetooth.A2dpProfile;
import com.android.settingslib.bluetooth.HearingAidProfile;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.media.MediaOutputSliceConstants;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

import java.util.ArrayList;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowBluetoothUtils.class})
@Config(shadows = {ShadowBluetoothUtils.class,
        MediaOutputIndicatorSliceTest.ShadowSliceBackgroundWorker.class})
public class MediaOutputIndicatorSliceTest {

    private static final String TEST_A2DP_DEVICE_NAME = "Test_A2DP_BT_Device_NAME";
    private static final String TEST_HAP_DEVICE_NAME = "Test_HAP_BT_Device_NAME";
    private static final String TEST_A2DP_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
    private static final String TEST_HAP_DEVICE_ADDRESS = "00:B2:B2:B2:B2:B2";
    private static final String TEST_PACKAGE_NAME = "com.test";

    private static MediaOutputIndicatorWorker sMediaOutputIndicatorWorker;

    @Mock
    private A2dpProfile mA2dpProfile;
@@ -70,6 +88,8 @@ public class MediaOutputIndicatorSliceTest {
    private LocalBluetoothManager mLocalBluetoothManager;
    @Mock
    private LocalBluetoothProfileManager mLocalBluetoothProfileManager;
    @Mock
    private MediaController mMediaController;

    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothDevice mA2dpDevice;
@@ -79,6 +99,7 @@ public class MediaOutputIndicatorSliceTest {
    private List<BluetoothDevice> mDevicesList;
    private MediaOutputIndicatorSlice mMediaOutputIndicatorSlice;
    private AudioManager mAudioManager;
    private MediaSession.Token mToken;

    @Before
    public void setUp() throws Exception {
@@ -86,9 +107,11 @@ public class MediaOutputIndicatorSliceTest {
        mContext = spy(RuntimeEnvironment.application);
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        mAudioManager.setMode(AudioManager.MODE_NORMAL);
        sMediaOutputIndicatorWorker = spy(new MediaOutputIndicatorWorker(mContext,
                MEDIA_OUTPUT_INDICATOR_SLICE_URI));
        mToken = new MediaSession.Token(null);
        // Set-up specs for SliceMetadata.
        SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);

        // Setup Bluetooth environment
        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
        mBluetoothManager = new BluetoothManager(mContext);
@@ -196,4 +219,45 @@ public class MediaOutputIndicatorSliceTest {
        final SliceMetadata metadata = SliceMetadata.from(mContext, mediaSlice);
        assertThat(metadata.isErrorSlice()).isTrue();
    }

    @Test
    public void onNotifyChange_withActiveLocalMedia_verifyIntentExtra() {
        when(mMediaController.getSessionToken()).thenReturn(mToken);
        when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
        doReturn(mMediaController).when(sMediaOutputIndicatorWorker)
                .getActiveLocalMediaController();

        final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
        mMediaOutputIndicatorSlice.onNotifyChange(new Intent());
        verify(mContext).startActivity(intentCaptor.capture());

        assertThat(TextUtils.equals(TEST_PACKAGE_NAME, intentCaptor.getValue().getStringExtra(
                MediaOutputSliceConstants.EXTRA_PACKAGE_NAME))).isTrue();
        assertThat(mToken == intentCaptor.getValue().getExtras().getParcelable(
                MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN)).isTrue();
    }

    @Test
    public void onNotifyChange_withoutActiveLocalMedia_verifyIntentExtra() {
        doReturn(mMediaController).when(sMediaOutputIndicatorWorker)
                .getActiveLocalMediaController();

        final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
        mMediaOutputIndicatorSlice.onNotifyChange(new Intent());
        verify(mContext).startActivity(intentCaptor.capture());

        assertThat(TextUtils.isEmpty(intentCaptor.getValue().getStringExtra(
                MediaOutputSliceConstants.EXTRA_PACKAGE_NAME))).isTrue();
        assertThat(intentCaptor.getValue().getExtras().getParcelable(
                MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN) == null).isTrue();
    }

    @Implements(SliceBackgroundWorker.class)
    public static class ShadowSliceBackgroundWorker {

        @Implementation
        public static SliceBackgroundWorker getInstance(Uri uri) {
            return sMediaOutputIndicatorWorker;
        }
    }
}
+87 −8
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.settings.media;

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

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -28,7 +30,12 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.VolumeProvider;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.net.Uri;

import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
@@ -45,6 +52,9 @@ import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplication;

import java.util.ArrayList;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowBluetoothUtils.class})
public class MediaOutputIndicatorWorkerTest {
@@ -54,10 +64,18 @@ public class MediaOutputIndicatorWorkerTest {
    private BluetoothEventManager mBluetoothEventManager;
    @Mock
    private LocalBluetoothManager mLocalBluetoothManager;
    @Mock
    private MediaSessionManager mMediaSessionManager;
    @Mock
    private MediaController mMediaController;

    private Context mContext;
    private MediaOutputIndicatorWorker mMediaDeviceUpdateWorker;
    private MediaOutputIndicatorWorker mMediaOutputIndicatorWorker;
    private ShadowApplication mShadowApplication;
    private ContentResolver mResolver;
    private List<MediaController> mMediaControllers = new ArrayList<>();
    private PlaybackState mPlaybackState;
    private MediaController.PlaybackInfo mPlaybackInfo;

    @Before
    public void setUp() {
@@ -66,7 +84,10 @@ public class MediaOutputIndicatorWorkerTest {
        mContext = spy(RuntimeEnvironment.application);
        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
        when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
        mMediaDeviceUpdateWorker = new MediaOutputIndicatorWorker(mContext, URI);
        mMediaOutputIndicatorWorker = new MediaOutputIndicatorWorker(mContext, URI);
        when(mContext.getSystemService(MediaSessionManager.class)).thenReturn(mMediaSessionManager);
        mMediaControllers.add(mMediaController);
        when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers);

        mResolver = mock(ContentResolver.class);
        doReturn(mResolver).when(mContext).getContentResolver();
@@ -74,22 +95,22 @@ public class MediaOutputIndicatorWorkerTest {

    @Test
    public void onSlicePinned_registerCallback() {
        mMediaDeviceUpdateWorker.onSlicePinned();
        verify(mBluetoothEventManager).registerCallback(mMediaDeviceUpdateWorker);
        mMediaOutputIndicatorWorker.onSlicePinned();
        verify(mBluetoothEventManager).registerCallback(mMediaOutputIndicatorWorker);
        verify(mContext).registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class));
    }

    @Test
    public void onSliceUnpinned_unRegisterCallback() {
        mMediaDeviceUpdateWorker.onSlicePinned();
        mMediaDeviceUpdateWorker.onSliceUnpinned();
        verify(mBluetoothEventManager).unregisterCallback(mMediaDeviceUpdateWorker);
        mMediaOutputIndicatorWorker.onSlicePinned();
        mMediaOutputIndicatorWorker.onSliceUnpinned();
        verify(mBluetoothEventManager).unregisterCallback(mMediaOutputIndicatorWorker);
        verify(mContext).unregisterReceiver(any(BroadcastReceiver.class));
    }

    @Test
    public void onReceive_shouldNotifyChange() {
        mMediaDeviceUpdateWorker.onSlicePinned();
        mMediaOutputIndicatorWorker.onSlicePinned();

        final Intent intent = new Intent(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
        for (BroadcastReceiver receiver : mShadowApplication.getReceiversForIntent(intent)) {
@@ -98,4 +119,62 @@ public class MediaOutputIndicatorWorkerTest {

        verify(mResolver).notifyChange(URI, null);
    }

    @Test
    public void getActiveLocalMediaController_localMediaPlaying_returnController() {
        mPlaybackInfo = new MediaController.PlaybackInfo(
                MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
                VolumeProvider.VOLUME_CONTROL_ABSOLUTE,
                100,
                10,
                new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(),
                null);
        mPlaybackState = new PlaybackState.Builder()
                .setState(PlaybackState.STATE_PLAYING, 0, 1)
                .build();

        when(mMediaController.getPlaybackInfo()).thenReturn(mPlaybackInfo);
        when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState);

        assertThat(mMediaOutputIndicatorWorker.getActiveLocalMediaController()).isEqualTo(
                mMediaController);
    }

    @Test
    public void getActiveLocalMediaController_remoteMediaPlaying_returnNull() {
        mPlaybackInfo = new MediaController.PlaybackInfo(
                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
                VolumeProvider.VOLUME_CONTROL_ABSOLUTE,
                100,
                10,
                new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(),
                null);
        mPlaybackState = new PlaybackState.Builder()
                .setState(PlaybackState.STATE_PLAYING, 0, 1)
                .build();

        when(mMediaController.getPlaybackInfo()).thenReturn(mPlaybackInfo);
        when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState);

        assertThat(mMediaOutputIndicatorWorker.getActiveLocalMediaController()).isNull();
    }

    @Test
    public void getActiveLocalMediaController_localMediaStopped_returnNull() {
        mPlaybackInfo = new MediaController.PlaybackInfo(
                MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
                VolumeProvider.VOLUME_CONTROL_ABSOLUTE,
                100,
                10,
                new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(),
                null);
        mPlaybackState = new PlaybackState.Builder()
                .setState(PlaybackState.STATE_STOPPED, 0, 1)
                .build();

        when(mMediaController.getPlaybackInfo()).thenReturn(mPlaybackInfo);
        when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState);

        assertThat(mMediaOutputIndicatorWorker.getActiveLocalMediaController()).isNull();
    }
}