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

Commit d48be9a5 authored by hughchen's avatar hughchen
Browse files

Implement MediaOutputSlice

Implement MediaOutputSlice that used to show the MediaDevice list and
switch the device to transfer the media.

Bug: 121083246
Test: make -j RunSettingsRoboTests
Change-Id: I0d57cc75ca1fc8eae2d943819f84b1ec8b608255
parent 21778135
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -10566,4 +10566,8 @@
        <item quantity="other"><xliff:g id="notification_channel_count">%1$d</xliff:g> notification channels. Tap to manage all.</item>
    </plurals>
    <!-- Title for the Switch output dialog (settings panel) with media related devices [CHAR LIMIT=50] -->
    <string name="media_output_panel_title">Switch output</string>
    <!-- Summary for represent which device is playing media [CHAR LIMIT=NONE] -->
    <string name="media_output_panel_summary_of_playing_device">Currently playing on <xliff:g id="device_name" example="Bose headphone">%1$s</xliff:g></string>
</resources>
+107 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.settings.media;

import android.content.Context;
import android.net.Uri;

import androidx.annotation.VisibleForTesting;

import com.android.settings.slices.SliceBackgroundWorker;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;

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

/**
 * SliceBackgroundWorker for get MediaDevice list and handle MediaDevice state change event.
 */
public class MediaDeviceUpdateWorker extends SliceBackgroundWorker
        implements LocalMediaManager.DeviceCallback {

    private final Context mContext;
    private final List<MediaDevice> mMediaDevices = new ArrayList<>();

    private String mPackageName;

    @VisibleForTesting
    LocalMediaManager mLocalMediaManager;

    public MediaDeviceUpdateWorker(Context context, Uri uri) {
        super(context, uri);
        mContext = context;
    }

    public void setPackageName(String packageName) {
        mPackageName = packageName;
    }

    @Override
    protected void onSlicePinned() {
        mMediaDevices.clear();
        if (mLocalMediaManager == null) {
            mLocalMediaManager = new LocalMediaManager(mContext, mPackageName, null);
        }

        mLocalMediaManager.registerCallback(this);
        mLocalMediaManager.startScan();
    }

    @Override
    protected void onSliceUnpinned() {
        mLocalMediaManager.unregisterCallback(this);
        mLocalMediaManager.stopScan();
    }

    @Override
    public void close() {

    }

    @Override
    public void onDeviceListUpdate(List<MediaDevice> devices) {
        buildMediaDevices(devices);
        notifySliceChange();
    }

    private void buildMediaDevices(List<MediaDevice> devices) {
        mMediaDevices.clear();
        mMediaDevices.addAll(devices);
    }

    @Override
    public void onSelectedDeviceStateChanged(MediaDevice device, int state) {
        notifySliceChange();
    }

    public List<MediaDevice> getMediaDevices() {
        return new ArrayList<>(mMediaDevices);
    }

    public void connectDevice(MediaDevice device) {
        mLocalMediaManager.connectDevice(device);
    }

    public MediaDevice getMediaDeviceById(String id) {
        return mLocalMediaManager.getMediaDeviceById(mMediaDevices, id);
    }

    public MediaDevice getCurrentConnectedMediaDevice() {
        return mLocalMediaManager.getCurrentConnectedDevice();
    }
}
+195 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.settings.media;

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

import android.annotation.ColorInt;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.UserHandle;
import android.util.IconDrawableFactory;
import android.util.Log;

import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import androidx.slice.Slice;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.SliceAction;

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.media.MediaDevice;

import java.util.List;

/**
 * Show the Media device that can be transfer the media.
 */
public class MediaOutputSlice implements CustomSliceable {

    private static final String TAG = "MediaOutputSlice";
    private static final String MEDIA_DEVICE_ID = "media_device_id";

    public static final String MEDIA_PACKAGE_NAME = "media_package_name";

    private final Context mContext;

    private MediaDeviceUpdateWorker mWorker;
    private String mPackageName;
    private IconDrawableFactory mIconDrawableFactory;

    public MediaOutputSlice(Context context) {
        mContext = context;
        mPackageName = getUri().getQueryParameter(MEDIA_PACKAGE_NAME);
        mIconDrawableFactory = IconDrawableFactory.newInstance(mContext);
    }

    @VisibleForTesting
    void init(String packageName, MediaDeviceUpdateWorker worker, IconDrawableFactory factory) {
        mPackageName = packageName;
        mWorker = worker;
        mIconDrawableFactory = factory;
    }

    @Override
    public Slice getSlice() {
        final PackageManager pm = mContext.getPackageManager();

        final List<MediaDevice> devices = getMediaDevices();
        final CharSequence title = Utils.getApplicationLabel(mContext, mPackageName);
        final CharSequence summary =
                mContext.getString(R.string.media_output_panel_summary_of_playing_device,
                        getConnectedDeviceName());

        final Drawable drawable =
                Utils.getBadgedIcon(mIconDrawableFactory, pm, mPackageName, UserHandle.myUserId());
        final IconCompat icon = IconCompat.createWithBitmap(getBitmapFromDrawable(drawable));

        @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext);
        final SliceAction primarySliceAction = SliceAction.createDeeplink(getPrimaryAction(), icon,
                ListBuilder.ICON_IMAGE, title);

        final ListBuilder listBuilder = new ListBuilder(mContext, MEDIA_OUTPUT_SLICE_URI,
                ListBuilder.INFINITY)
                .setAccentColor(color)
                .addRow(new ListBuilder.RowBuilder()
                        .setTitleItem(icon, ListBuilder.ICON_IMAGE)
                        .setTitle(title)
                        .setSubtitle(summary)
                        .setPrimaryAction(primarySliceAction));

        for (MediaDevice device : devices) {
            listBuilder.addRow(getMediaDeviceRow(device));
        }

        return listBuilder.build();
    }

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

    private List<MediaDevice> getMediaDevices() {
        List<MediaDevice> devices = getWorker().getMediaDevices();
        return devices;
    }

    private String getConnectedDeviceName() {
        final MediaDevice device = getWorker().getCurrentConnectedMediaDevice();
        return device != null ? device.getName() : "";
    }

    private PendingIntent getPrimaryAction() {
        final PackageManager pm = mContext.getPackageManager();
        final Intent launchIntent = pm.getLaunchIntentForPackage(mPackageName);
        final Intent intent = launchIntent;
        return PendingIntent.getActivity(mContext, 0  /* requestCode */, intent, 0  /* flags */);
    }

    private Bitmap getBitmapFromDrawable(Drawable drawable) {
        final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
                drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(bitmap);

        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);

        return bitmap;
    }

    private ListBuilder.RowBuilder getMediaDeviceRow(MediaDevice device) {
        final String title = device.getName();
        final PendingIntent broadcastAction =
                getBroadcastIntent(mContext, device.getId(), device.hashCode());
        final IconCompat deviceIcon = IconCompat.createWithResource(mContext, device.getIcon());
        final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder()
                .setTitleItem(deviceIcon, ListBuilder.ICON_IMAGE)
                .setPrimaryAction(SliceAction.create(broadcastAction, deviceIcon,
                        ListBuilder.ICON_IMAGE, title))
                .setTitle(title);

        return rowBuilder;
    }

    private PendingIntent getBroadcastIntent(Context context, String id, int requestCode) {
        final Intent intent = new Intent(getUri().toString());
        intent.setClass(context, SliceBroadcastReceiver.class);
        intent.putExtra(MEDIA_DEVICE_ID, id);
        return PendingIntent.getBroadcast(context, requestCode /* requestCode */, intent,
                PendingIntent.FLAG_CANCEL_CURRENT);
    }

    @Override
    public Uri getUri() {
        return MEDIA_OUTPUT_SLICE_URI;
    }

    @Override
    public void onNotifyChange(Intent intent) {
        final MediaDeviceUpdateWorker worker = getWorker();
        final String id = intent != null ? intent.getStringExtra(MEDIA_DEVICE_ID) : "";
        final MediaDevice device = worker.getMediaDeviceById(id);
        if (device != null) {
            Log.d(TAG, "onNotifyChange() device name : " + device.getName());
            worker.connectDevice(device);
        }
    }

    @Override
    public Intent getIntent() {
        return null;
    }

    @Override
    public Class getBackgroundWorkerClass() {
        return MediaDeviceUpdateWorker.class;
    }
}
+132 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.settings.media;

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

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

import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;

import com.android.settingslib.media.MediaDevice;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

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

@RunWith(RobolectricTestRunner.class)
public class MediaDeviceUpdateWorkerTest {

    private static final Uri URI = Uri.parse("content://com.android.settings.slices/test");
    private static final String TEST_DEVICE_1_ID = "test_device_1_id";
    private static final String TEST_DEVICE_2_ID = "test_device_2_id";
    private static final String TEST_DEVICE_3_ID = "test_device_3_id";

    private final List<MediaDevice> mMediaDevices = new ArrayList<>();

    private MediaDeviceUpdateWorker mMediaDeviceUpdateWorker;
    private ContentResolver mResolver;
    private Context mContext;
    private MediaDevice mMediaDevice1;
    private MediaDevice mMediaDevice2;

    @Before
    public void setUp() {
        mContext = spy(RuntimeEnvironment.application);
        mMediaDeviceUpdateWorker = new MediaDeviceUpdateWorker(mContext, URI);
        mResolver = mock(ContentResolver.class);

        mMediaDevice1 = mock(MediaDevice.class);
        when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_1_ID);
        mMediaDevice2 = mock(MediaDevice.class);
        when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
        mMediaDevices.add(mMediaDevice1);
        mMediaDevices.add(mMediaDevice2);

        doReturn(mResolver).when(mContext).getContentResolver();
    }

    @Test
    public void onDeviceListUpdate_shouldNotifyChange() {
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mMediaDevices);

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

    @Test
    public void onSelectedDeviceStateChanged_shouldNotifyChange() {
        mMediaDeviceUpdateWorker.onSelectedDeviceStateChanged(null, 0);

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

    @Test
    public void onDeviceListUpdate_sameDeviceList_shouldBeEqual() {
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mMediaDevices);

        final List<MediaDevice> newDevices = new ArrayList<>();
        newDevices.add(mMediaDevice1);
        newDevices.add(mMediaDevice2);

        mMediaDeviceUpdateWorker.onDeviceListUpdate(newDevices);
        final List<MediaDevice> devices = mMediaDeviceUpdateWorker.getMediaDevices();

        assertThat(devices.get(0).getId()).isEqualTo(newDevices.get(0).getId());
        assertThat(devices.get(1).getId()).isEqualTo(newDevices.get(1).getId());
    }

    @Test
    public void onDeviceListUpdate_add1DeviceToDeviceList_shouldBeEqual() {
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mMediaDevices);

        final List<MediaDevice> newDevices = new ArrayList<>();
        final MediaDevice device3 = mock(MediaDevice.class);
        when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_3_ID);
        newDevices.add(mMediaDevice1);
        newDevices.add(mMediaDevice2);
        newDevices.add(device3);

        mMediaDeviceUpdateWorker.onDeviceListUpdate(newDevices);
        final List<MediaDevice> devices = mMediaDeviceUpdateWorker.getMediaDevices();

        assertThat(devices.size()).isEqualTo(newDevices.size());
    }

    @Test
    public void onDeviceListUpdate_less1DeviceToDeviceList_shouldBeEqual() {
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mMediaDevices);

        final List<MediaDevice> newDevices = new ArrayList<>();
        newDevices.add(mMediaDevice1);

        mMediaDeviceUpdateWorker.onDeviceListUpdate(newDevices);
        final List<MediaDevice> devices = mMediaDeviceUpdateWorker.getMediaDevices();

        assertThat(devices.size()).isEqualTo(newDevices.size());
    }
}
+157 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.settings.media;

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

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

import static org.mockito.ArgumentMatchers.anyInt;
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.when;

import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.util.IconDrawableFactory;

import androidx.slice.Slice;
import androidx.slice.SliceMetadata;
import androidx.slice.SliceProvider;
import androidx.slice.core.SliceAction;
import androidx.slice.widget.SliceLiveData;

import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

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

@RunWith(RobolectricTestRunner.class)
public class MediaOutputSliceTest {

    private static final String TEST_PACKAGE_NAME = "com.fake.android.music";
    private static final String TEST_LABEL = "Test app";
    private static final String TEST_DEVICE_1_ID = "test_device_1_id";

    @Mock
    private PackageManager mPackageManager;
    @Mock
    private ApplicationInfo mApplicationInfo;
    @Mock
    private ApplicationInfo mApplicationInfo2;
    @Mock
    private LocalMediaManager mLocalMediaManager;
    @Mock
    private IconDrawableFactory mIconDrawableFactory;
    @Mock
    private Drawable mTestDrawable;

    private final List<MediaDevice> mDevices = new ArrayList<>();

    private Context mContext;
    private MediaOutputSlice mMediaOutputSlice;
    private MediaDeviceUpdateWorker mMediaDeviceUpdateWorker;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mContext = spy(RuntimeEnvironment.application);

        when(mContext.getPackageManager()).thenReturn(mPackageManager);
        when(mPackageManager.getApplicationInfo(eq(TEST_PACKAGE_NAME), anyInt()))
                .thenReturn(mApplicationInfo);
        when(mPackageManager.getApplicationInfoAsUser(eq(TEST_PACKAGE_NAME), anyInt(), anyInt()))
                .thenReturn(mApplicationInfo2);
        when(mApplicationInfo.loadLabel(mPackageManager)).thenReturn(TEST_LABEL);
        when(mIconDrawableFactory.getBadgedIcon(mApplicationInfo2, UserHandle.myUserId()))
                .thenReturn(mTestDrawable);
        when(mTestDrawable.getIntrinsicWidth()).thenReturn(100);
        when(mTestDrawable.getIntrinsicHeight()).thenReturn(100);

        // Set-up specs for SliceMetadata.
        SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);

        mMediaOutputSlice = new MediaOutputSlice(mContext);
        mMediaDeviceUpdateWorker = new MediaDeviceUpdateWorker(mContext, MEDIA_OUTPUT_SLICE_URI);
        mMediaDeviceUpdateWorker.setPackageName(TEST_PACKAGE_NAME);
        mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);
        mMediaDeviceUpdateWorker.mLocalMediaManager = mLocalMediaManager;
        mMediaOutputSlice.init(TEST_PACKAGE_NAME, mMediaDeviceUpdateWorker, mIconDrawableFactory);
    }

    @Test
    public void getSlice_shouldHaveAppTitle() {
        final Slice mediaSlice = mMediaOutputSlice.getSlice();
        final SliceMetadata metadata = SliceMetadata.from(mContext, mediaSlice);

        final SliceAction primaryAction = metadata.getPrimaryAction();
        assertThat(primaryAction.getTitle().toString()).isEqualTo(TEST_LABEL);
    }

    @Test
    public void onNotifyChange_foundMediaDevice_connect() {
        mDevices.clear();
        final MediaDevice device = mock(MediaDevice.class);
        when(device.getId()).thenReturn(TEST_DEVICE_1_ID);
        when(mLocalMediaManager.getMediaDeviceById(mDevices, TEST_DEVICE_1_ID)).thenReturn(device);
        mDevices.add(device);

        mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);

        final Intent intent = new Intent();
        intent.putExtra("media_device_id", TEST_DEVICE_1_ID);

        mMediaOutputSlice.onNotifyChange(intent);

        verify(mLocalMediaManager).connectDevice(device);
    }

    @Test
    public void onNotifyChange_notFoundMediaDevice_doNothing() {
        mDevices.clear();
        final MediaDevice device = mock(MediaDevice.class);
        when(device.getId()).thenReturn(TEST_DEVICE_1_ID);
        when(mLocalMediaManager.getMediaDeviceById(mDevices, TEST_DEVICE_1_ID)).thenReturn(device);
        mDevices.add(device);

        mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);

        final Intent intent = new Intent();
        intent.putExtra("media_device_id", "fake_123");

        mMediaOutputSlice.onNotifyChange(intent);

        verify(mLocalMediaManager, never()).connectDevice(device);
    }
}