Merge "Update output switcher behavior and UI design"
This commit is contained in:
committed by
Android (Google) Code Review
commit
94240c5a52
@@ -70,7 +70,7 @@ public class MediaDeviceUpdateWorker extends SliceBackgroundWorker
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
mLocalMediaManager = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -20,13 +20,11 @@ import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE
|
|||||||
|
|
||||||
import android.annotation.ColorInt;
|
import android.annotation.ColorInt;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.UserHandle;
|
import android.text.TextUtils;
|
||||||
import android.util.IconDrawableFactory;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
@@ -58,37 +56,48 @@ public class MediaOutputSlice implements CustomSliceable {
|
|||||||
|
|
||||||
private MediaDeviceUpdateWorker mWorker;
|
private MediaDeviceUpdateWorker mWorker;
|
||||||
private String mPackageName;
|
private String mPackageName;
|
||||||
private IconDrawableFactory mIconDrawableFactory;
|
|
||||||
|
|
||||||
public MediaOutputSlice(Context context) {
|
public MediaOutputSlice(Context context) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mPackageName = getUri().getQueryParameter(MEDIA_PACKAGE_NAME);
|
mPackageName = getUri().getQueryParameter(MEDIA_PACKAGE_NAME);
|
||||||
mIconDrawableFactory = IconDrawableFactory.newInstance(mContext);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void init(String packageName, MediaDeviceUpdateWorker worker, IconDrawableFactory factory) {
|
void init(String packageName, MediaDeviceUpdateWorker worker) {
|
||||||
mPackageName = packageName;
|
mPackageName = packageName;
|
||||||
mWorker = worker;
|
mWorker = worker;
|
||||||
mIconDrawableFactory = factory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Slice getSlice() {
|
public Slice getSlice() {
|
||||||
final PackageManager pm = mContext.getPackageManager();
|
final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
if (!adapter.isEnabled()) {
|
||||||
|
Log.d(TAG, "getSlice() Bluetooth is off");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final List<MediaDevice> devices = getMediaDevices();
|
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 = Utils.createIconWithDrawable(drawable);
|
|
||||||
|
|
||||||
@ColorInt final int color = Utils.getColorAccentDefaultColor(mContext);
|
@ColorInt final int color = Utils.getColorAccentDefaultColor(mContext);
|
||||||
final SliceAction primarySliceAction = SliceAction.createDeeplink(getPrimaryAction(), icon,
|
|
||||||
|
final MediaDevice connectedDevice = getWorker().getCurrentConnectedMediaDevice();
|
||||||
|
final ListBuilder listBuilder = buildActiveDeviceHeader(color, connectedDevice);
|
||||||
|
|
||||||
|
for (MediaDevice device : devices) {
|
||||||
|
if (!TextUtils.equals(connectedDevice.getId(), device.getId())) {
|
||||||
|
listBuilder.addRow(getMediaDeviceRow(device));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListBuilder buildActiveDeviceHeader(@ColorInt int color, MediaDevice device) {
|
||||||
|
final String title = device.getName();
|
||||||
|
final IconCompat icon = IconCompat.createWithResource(mContext, device.getIcon());
|
||||||
|
|
||||||
|
final PendingIntent broadcastAction =
|
||||||
|
getBroadcastIntent(mContext, device.getId(), device.hashCode());
|
||||||
|
final SliceAction primarySliceAction = SliceAction.createDeeplink(broadcastAction, icon,
|
||||||
ListBuilder.ICON_IMAGE, title);
|
ListBuilder.ICON_IMAGE, title);
|
||||||
|
|
||||||
final ListBuilder listBuilder = new ListBuilder(mContext, MEDIA_OUTPUT_SLICE_URI,
|
final ListBuilder listBuilder = new ListBuilder(mContext, MEDIA_OUTPUT_SLICE_URI,
|
||||||
@@ -97,14 +106,10 @@ public class MediaOutputSlice implements CustomSliceable {
|
|||||||
.addRow(new ListBuilder.RowBuilder()
|
.addRow(new ListBuilder.RowBuilder()
|
||||||
.setTitleItem(icon, ListBuilder.ICON_IMAGE)
|
.setTitleItem(icon, ListBuilder.ICON_IMAGE)
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setSubtitle(summary)
|
.setSubtitle(device.getSummary())
|
||||||
.setPrimaryAction(primarySliceAction));
|
.setPrimaryAction(primarySliceAction));
|
||||||
|
|
||||||
for (MediaDevice device : devices) {
|
return listBuilder;
|
||||||
listBuilder.addRow(getMediaDeviceRow(device));
|
|
||||||
}
|
|
||||||
|
|
||||||
return listBuilder.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaDeviceUpdateWorker getWorker() {
|
private MediaDeviceUpdateWorker getWorker() {
|
||||||
@@ -120,18 +125,6 @@ public class MediaOutputSlice implements CustomSliceable {
|
|||||||
return devices;
|
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 ListBuilder.RowBuilder getMediaDeviceRow(MediaDevice device) {
|
private ListBuilder.RowBuilder getMediaDeviceRow(MediaDevice device) {
|
||||||
final String title = device.getName();
|
final String title = device.getName();
|
||||||
final PendingIntent broadcastAction =
|
final PendingIntent broadcastAction =
|
||||||
@@ -141,7 +134,8 @@ public class MediaOutputSlice implements CustomSliceable {
|
|||||||
.setTitleItem(deviceIcon, ListBuilder.ICON_IMAGE)
|
.setTitleItem(deviceIcon, ListBuilder.ICON_IMAGE)
|
||||||
.setPrimaryAction(SliceAction.create(broadcastAction, deviceIcon,
|
.setPrimaryAction(SliceAction.create(broadcastAction, deviceIcon,
|
||||||
ListBuilder.ICON_IMAGE, title))
|
ListBuilder.ICON_IMAGE, title))
|
||||||
.setTitle(title);
|
.setTitle(title)
|
||||||
|
.setSubtitle(device.getSummary());
|
||||||
|
|
||||||
return rowBuilder;
|
return rowBuilder;
|
||||||
}
|
}
|
||||||
|
@@ -16,13 +16,11 @@
|
|||||||
|
|
||||||
package com.android.settings.panel;
|
package com.android.settings.panel;
|
||||||
|
|
||||||
import static com.android.settingslib.media.MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT;
|
|
||||||
import static com.android.settingslib.media.MediaOutputSliceConstants.EXTRA_PACKAGE_NAME;
|
import static com.android.settingslib.media.MediaOutputSliceConstants.EXTRA_PACKAGE_NAME;
|
||||||
|
|
||||||
import android.app.settings.SettingsEnums;
|
import android.app.settings.SettingsEnums;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@@ -75,15 +73,8 @@ public class SettingsPanelActivity extends FragmentActivity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String mediaPackageName =
|
// We will use it once media output switch panel support remote device.
|
||||||
callingIntent.getStringExtra(EXTRA_PACKAGE_NAME);
|
final String mediaPackageName = callingIntent.getStringExtra(EXTRA_PACKAGE_NAME);
|
||||||
|
|
||||||
if (TextUtils.equals(ACTION_MEDIA_OUTPUT, callingIntent.getAction())
|
|
||||||
&& TextUtils.isEmpty(mediaPackageName)) {
|
|
||||||
Log.e(TAG, "Missing EXTRA_PACKAGE_NAME, closing Panel Activity");
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(R.layout.settings_panel);
|
setContentView(R.layout.settings_panel);
|
||||||
|
|
||||||
|
@@ -21,21 +21,15 @@ import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE
|
|||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
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.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
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.Slice;
|
||||||
import androidx.slice.SliceMetadata;
|
import androidx.slice.SliceMetadata;
|
||||||
@@ -43,6 +37,7 @@ import androidx.slice.SliceProvider;
|
|||||||
import androidx.slice.core.SliceAction;
|
import androidx.slice.core.SliceAction;
|
||||||
import androidx.slice.widget.SliceLiveData;
|
import androidx.slice.widget.SliceLiveData;
|
||||||
|
|
||||||
|
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
|
||||||
import com.android.settingslib.media.LocalMediaManager;
|
import com.android.settingslib.media.LocalMediaManager;
|
||||||
import com.android.settingslib.media.MediaDevice;
|
import com.android.settingslib.media.MediaDevice;
|
||||||
|
|
||||||
@@ -53,70 +48,64 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
import org.robolectric.RuntimeEnvironment;
|
import org.robolectric.RuntimeEnvironment;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
import org.robolectric.shadow.api.Shadow;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
@Config(shadows = {ShadowBluetoothAdapter.class})
|
||||||
public class MediaOutputSliceTest {
|
public class MediaOutputSliceTest {
|
||||||
|
|
||||||
private static final String TEST_PACKAGE_NAME = "com.fake.android.music";
|
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";
|
private static final String TEST_DEVICE_1_ID = "test_device_1_id";
|
||||||
|
private static final String TEST_DEVICE_1_NAME = "test_device_1_name";
|
||||||
|
private static final int TEST_DEVICE_1_ICON =
|
||||||
|
com.android.internal.R.drawable.ic_bt_headphones_a2dp;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private PackageManager mPackageManager;
|
|
||||||
@Mock
|
|
||||||
private ApplicationInfo mApplicationInfo;
|
|
||||||
@Mock
|
|
||||||
private ApplicationInfo mApplicationInfo2;
|
|
||||||
@Mock
|
@Mock
|
||||||
private LocalMediaManager mLocalMediaManager;
|
private LocalMediaManager mLocalMediaManager;
|
||||||
@Mock
|
|
||||||
private IconDrawableFactory mIconDrawableFactory;
|
|
||||||
@Mock
|
|
||||||
private Drawable mTestDrawable;
|
|
||||||
|
|
||||||
private final List<MediaDevice> mDevices = new ArrayList<>();
|
private final List<MediaDevice> mDevices = new ArrayList<>();
|
||||||
|
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
private MediaOutputSlice mMediaOutputSlice;
|
private MediaOutputSlice mMediaOutputSlice;
|
||||||
private MediaDeviceUpdateWorker mMediaDeviceUpdateWorker;
|
private MediaDeviceUpdateWorker mMediaDeviceUpdateWorker;
|
||||||
|
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
mContext = spy(RuntimeEnvironment.application);
|
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.
|
// Set-up specs for SliceMetadata.
|
||||||
SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
|
SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
|
||||||
|
// Setup BluetoothAdapter
|
||||||
|
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||||
|
mShadowBluetoothAdapter.setEnabled(true);
|
||||||
|
|
||||||
mMediaOutputSlice = new MediaOutputSlice(mContext);
|
mMediaOutputSlice = new MediaOutputSlice(mContext);
|
||||||
mMediaDeviceUpdateWorker = new MediaDeviceUpdateWorker(mContext, MEDIA_OUTPUT_SLICE_URI);
|
mMediaDeviceUpdateWorker = new MediaDeviceUpdateWorker(mContext, MEDIA_OUTPUT_SLICE_URI);
|
||||||
mMediaDeviceUpdateWorker.setPackageName(TEST_PACKAGE_NAME);
|
mMediaDeviceUpdateWorker.setPackageName(TEST_PACKAGE_NAME);
|
||||||
mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);
|
mMediaDeviceUpdateWorker.onDeviceListUpdate(mDevices);
|
||||||
mMediaDeviceUpdateWorker.mLocalMediaManager = mLocalMediaManager;
|
mMediaDeviceUpdateWorker.mLocalMediaManager = mLocalMediaManager;
|
||||||
mMediaOutputSlice.init(TEST_PACKAGE_NAME, mMediaDeviceUpdateWorker, mIconDrawableFactory);
|
mMediaOutputSlice.init(TEST_PACKAGE_NAME, mMediaDeviceUpdateWorker);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getSlice_shouldHaveAppTitle() {
|
public void getSlice_shouldHaveActiveDeviceName() {
|
||||||
|
mDevices.clear();
|
||||||
|
final MediaDevice device = mock(MediaDevice.class);
|
||||||
|
when(device.getName()).thenReturn(TEST_DEVICE_1_NAME);
|
||||||
|
when(device.getIcon()).thenReturn(TEST_DEVICE_1_ICON);
|
||||||
|
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(device);
|
||||||
|
|
||||||
final Slice mediaSlice = mMediaOutputSlice.getSlice();
|
final Slice mediaSlice = mMediaOutputSlice.getSlice();
|
||||||
final SliceMetadata metadata = SliceMetadata.from(mContext, mediaSlice);
|
final SliceMetadata metadata = SliceMetadata.from(mContext, mediaSlice);
|
||||||
|
|
||||||
final SliceAction primaryAction = metadata.getPrimaryAction();
|
final SliceAction primaryAction = metadata.getPrimaryAction();
|
||||||
assertThat(primaryAction.getTitle().toString()).isEqualTo(TEST_LABEL);
|
assertThat(primaryAction.getTitle().toString()).isEqualTo(TEST_DEVICE_1_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@@ -76,15 +76,16 @@ public class SettingsPanelActivityTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void startMediaOutputSlice_withoutPackageName_bundleShouldNotHaveValue() {
|
public void startMediaOutputSlice_withoutPackageName_bundleShouldHaveValue() {
|
||||||
final Intent intent = new Intent()
|
final Intent intent = new Intent()
|
||||||
.setAction("com.android.settings.panel.action.MEDIA_OUTPUT");
|
.setAction("com.android.settings.panel.action.MEDIA_OUTPUT");
|
||||||
|
|
||||||
final SettingsPanelActivity activity =
|
final SettingsPanelActivity activity =
|
||||||
Robolectric.buildActivity(SettingsPanelActivity.class, intent).create().get();
|
Robolectric.buildActivity(SettingsPanelActivity.class, intent).create().get();
|
||||||
|
|
||||||
assertThat(activity.mBundle.containsKey(KEY_MEDIA_PACKAGE_NAME)).isFalse();
|
assertThat(activity.mBundle.containsKey(KEY_MEDIA_PACKAGE_NAME)).isTrue();
|
||||||
assertThat(activity.mBundle.containsKey(KEY_PANEL_TYPE_ARGUMENT)).isFalse();
|
assertThat(activity.mBundle.getString(KEY_PANEL_TYPE_ARGUMENT))
|
||||||
|
.isEqualTo("com.android.settings.panel.action.MEDIA_OUTPUT");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
Reference in New Issue
Block a user