From 6c1435114be7c1184d6b083960156e15e8cb10ea Mon Sep 17 00:00:00 2001 From: timhypeng Date: Thu, 3 Sep 2020 14:57:09 +0800 Subject: [PATCH 1/5] Add controller for Media operation -Access LocalMediaManger to display avilable output devices information -Access LocalMediaManger to do media operation, such as volume adjustment, switching output device, grouping -Access MediaController to show media content information -Add MediaOutputControllerTest for unit test Bug: 155822415 Test: atest MediaOutputControllerTest Change-Id: I9eb6e3b0a6e584637aecb4132dbc2b138c6d1530 --- packages/SystemUI/res/values/dimens.xml | 7 + packages/SystemUI/res/values/strings.xml | 15 + .../media/dialog/MediaOutputController.java | 445 ++++++++++++++++++ .../dialog/MediaOutputControllerTest.java | 348 ++++++++++++++ 4 files changed, 815 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 875fe1471b1c6..98e8cde40275d 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1366,4 +1366,11 @@ @*android:dimen/rounded_corner_radius @*android:dimen/rounded_corner_radius_top @*android:dimen/rounded_corner_radius_bottom + + + 11dp + 364dp + 52dp + 36dp + 16dp diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cca70f9aa5187..5ce40a8376639 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2799,4 +2799,19 @@ + + + Add outputs + + Group + + 1 device selected + + %1$d devices selected + + %1$s (disconnected) + + Couldn\'t connect. Try again. + + Pair new device diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java new file mode 100644 index 0000000000000..64d20a273931a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2020 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.systemui.media.dialog; + +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.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.media.MediaMetadata; +import android.media.RoutingSessionInfo; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; + +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.Utils; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.media.InfoMediaManager; +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; +import com.android.settingslib.media.MediaOutputSliceConstants; +import com.android.settingslib.utils.ThreadUtils; +import com.android.systemui.R; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.statusbar.phone.ShadeController; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.inject.Inject; + +/** + * Controller for media output dialog + */ +public class MediaOutputController implements LocalMediaManager.DeviceCallback{ + + private static final String TAG = "MediaOutputController"; + private static final boolean DEBUG = false; + + private final String mPackageName; + private final Context mContext; + private final MediaSessionManager mMediaSessionManager; + private final ShadeController mShadeController; + private final ActivityStarter mActivityStarter; + @VisibleForTesting + final List mMediaDevices = new CopyOnWriteArrayList<>(); + + private MediaController mMediaController; + @VisibleForTesting + Callback mCallback; + @VisibleForTesting + LocalMediaManager mLocalMediaManager; + + @Inject + public MediaOutputController(@NonNull Context context, String packageName, + MediaSessionManager mediaSessionManager, LocalBluetoothManager + lbm, ShadeController shadeController, ActivityStarter starter) { + mContext = context; + mPackageName = packageName; + mMediaSessionManager = mediaSessionManager; + mShadeController = shadeController; + mActivityStarter = starter; + InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm); + mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); + } + + void start(@NonNull Callback cb) { + mMediaDevices.clear(); + if (!TextUtils.isEmpty(mPackageName)) { + for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) { + if (TextUtils.equals(controller.getPackageName(), mPackageName)) { + mMediaController = controller; + mMediaController.unregisterCallback(mCb); + mMediaController.registerCallback(mCb); + break; + } + } + } + if (mMediaController == null) { + if (DEBUG) { + Log.d(TAG, "No media controller for " + mPackageName); + } + } + if (mLocalMediaManager == null) { + if (DEBUG) { + Log.d(TAG, "No local media manager " + mPackageName); + } + return; + } + mCallback = cb; + mLocalMediaManager.unregisterCallback(this); + mLocalMediaManager.stopScan(); + mLocalMediaManager.registerCallback(this); + mLocalMediaManager.startScan(); + } + + void stop() { + if (mMediaController != null) { + mMediaController.unregisterCallback(mCb); + } + if (mLocalMediaManager != null) { + mLocalMediaManager.unregisterCallback(this); + mLocalMediaManager.stopScan(); + } + mMediaDevices.clear(); + } + + @Override + public void onDeviceListUpdate(List devices) { + buildMediaDevices(devices); + mCallback.onRouteChanged(); + } + + @Override + public void onSelectedDeviceStateChanged(MediaDevice device, + @LocalMediaManager.MediaDeviceState int state) { + mCallback.onRouteChanged(); + } + + @Override + public void onDeviceAttributesChanged() { + mCallback.onRouteChanged(); + } + + @Override + public void onRequestFailed(int reason) { + mCallback.onRouteChanged(); + } + + CharSequence getHeaderTitle() { + if (mMediaController != null) { + final MediaMetadata metadata = mMediaController.getMetadata(); + if (metadata != null) { + return metadata.getDescription().getTitle(); + } + } + return mContext.getText(R.string.controls_media_title); + } + + CharSequence getHeaderSubTitle() { + if (mMediaController == null) { + return null; + } + final MediaMetadata metadata = mMediaController.getMetadata(); + if (metadata == null) { + return null; + } + return metadata.getDescription().getSubtitle(); + } + + IconCompat getHeaderIcon() { + if (mMediaController == null) { + return null; + } + final MediaMetadata metadata = mMediaController.getMetadata(); + if (metadata != null) { + final Bitmap bitmap = metadata.getDescription().getIconBitmap(); + if (bitmap != null) { + final Bitmap roundBitmap = Utils.convertCornerRadiusBitmap(mContext, bitmap, + (float) mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_icon_corner_radius)); + return IconCompat.createWithBitmap(roundBitmap); + } + } + if (DEBUG) { + Log.d(TAG, "Media meta data does not contain icon information"); + } + return getPackageIcon(); + } + + IconCompat getDeviceIconCompat(MediaDevice device) { + Drawable drawable = device.getIcon(); + if (drawable == null) { + if (DEBUG) { + Log.d(TAG, "getDeviceIconCompat() device : " + device.getName() + + ", drawable is null"); + } + // Use default Bluetooth device icon to handle getIcon() is null case. + drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp); + } + return BluetoothUtils.createIconWithDrawable(drawable); + } + + private IconCompat getPackageIcon() { + if (TextUtils.isEmpty(mPackageName)) { + return null; + } + try { + final Drawable drawable = mContext.getPackageManager().getApplicationIcon(mPackageName); + if (drawable instanceof BitmapDrawable) { + return IconCompat.createWithBitmap(((BitmapDrawable) drawable).getBitmap()); + } + 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 IconCompat.createWithBitmap(bitmap); + } catch (PackageManager.NameNotFoundException e) { + if (DEBUG) { + Log.e(TAG, "Package is not found. Unable to get package icon."); + } + } + return null; + } + + private void buildMediaDevices(List devices) { + // For the first time building list, to make sure the top device is the connected device. + if (mMediaDevices.isEmpty()) { + final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice(); + if (connectedMediaDevice == null) { + if (DEBUG) { + Log.d(TAG, "No connected media device."); + } + mMediaDevices.addAll(devices); + return; + } + for (MediaDevice device : devices) { + if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) { + mMediaDevices.add(0, device); + } else { + mMediaDevices.add(device); + } + } + return; + } + // To keep the same list order + final Collection targetMediaDevices = new ArrayList<>(); + for (MediaDevice originalDevice : mMediaDevices) { + for (MediaDevice newDevice : devices) { + if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) { + targetMediaDevices.add(newDevice); + break; + } + } + } + if (targetMediaDevices.size() != devices.size()) { + devices.removeAll(targetMediaDevices); + targetMediaDevices.addAll(devices); + } + mMediaDevices.clear(); + mMediaDevices.addAll(targetMediaDevices); + } + + void connectDevice(MediaDevice device) { + ThreadUtils.postOnBackgroundThread(() -> { + mLocalMediaManager.connectDevice(device); + }); + } + + Collection getMediaDevices() { + return mMediaDevices; + } + + MediaDevice getCurrentConnectedMediaDevice() { + return mLocalMediaManager.getCurrentConnectedDevice(); + } + + private MediaDevice getMediaDeviceById(String id) { + return mLocalMediaManager.getMediaDeviceById(new ArrayList<>(mMediaDevices), id); + } + + boolean addDeviceToPlayMedia(MediaDevice device) { + return mLocalMediaManager.addDeviceToPlayMedia(device); + } + + boolean removeDeviceFromPlayMedia(MediaDevice device) { + return mLocalMediaManager.removeDeviceFromPlayMedia(device); + } + + List getSelectableMediaDevice() { + return mLocalMediaManager.getSelectableMediaDevice(); + } + + List getSelectedMediaDevice() { + return mLocalMediaManager.getSelectedMediaDevice(); + } + + List getDeselectableMediaDevice() { + return mLocalMediaManager.getDeselectableMediaDevice(); + } + + boolean isDeviceIncluded(Collection deviceCollection, MediaDevice targetDevice) { + for (MediaDevice device : deviceCollection) { + if (TextUtils.equals(device.getId(), targetDevice.getId())) { + return true; + } + } + return false; + } + + void adjustSessionVolume(String sessionId, int volume) { + mLocalMediaManager.adjustSessionVolume(sessionId, volume); + } + + void adjustSessionVolume(int volume) { + mLocalMediaManager.adjustSessionVolume(volume); + } + + int getSessionVolumeMax() { + return mLocalMediaManager.getSessionVolumeMax(); + } + + int getSessionVolume() { + return mLocalMediaManager.getSessionVolume(); + } + + CharSequence getSessionName() { + return mLocalMediaManager.getSessionName(); + } + + void releaseSession() { + mLocalMediaManager.releaseSession(); + } + + List getActiveRemoteMediaDevices() { + final List sessionInfos = new ArrayList<>(); + for (RoutingSessionInfo info : mLocalMediaManager.getActiveMediaSession()) { + if (!info.isSystemSession()) { + sessionInfos.add(info); + } + } + return sessionInfos; + } + + void adjustVolume(MediaDevice device, int volume) { + ThreadUtils.postOnBackgroundThread(() -> { + device.requestSetVolume(volume); + }); + } + + String getPackageName() { + return mPackageName; + } + + boolean hasAdjustVolumeUserRestriction() { + if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) { + return true; + } + final UserManager um = mContext.getSystemService(UserManager.class); + return um.hasBaseUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME, + UserHandle.of(UserHandle.myUserId())); + } + + boolean isTransferring() { + for (MediaDevice device : mMediaDevices) { + if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) { + return true; + } + } + return false; + } + + boolean isZeroMode() { + if (mMediaDevices.size() == 1) { + final MediaDevice device = mMediaDevices.iterator().next(); + // Add "pair new" only when local output device exists + final int type = device.getDeviceType(); + if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE + || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE + || type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) { + return true; + } + } + return false; + } + + void launchBluetoothPairing() { + mCallback.dismissDialog(); + final ActivityStarter.OnDismissAction postKeyguardAction = () -> { + mContext.sendBroadcast(new Intent() + .setAction(MediaOutputSliceConstants.ACTION_LAUNCH_BLUETOOTH_PAIRING) + .setPackage(MediaOutputSliceConstants.SETTINGS_PACKAGE_NAME)); + mShadeController.animateCollapsePanels(); + return true; + }; + mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true); + } + + private final MediaController.Callback mCb = new MediaController.Callback() { + @Override + public void onMetadataChanged(MediaMetadata metadata) { + mCallback.onMediaChanged(); + } + + @Override + public void onPlaybackStateChanged(PlaybackState playbackState) { + final int state = playbackState.getState(); + if (state == PlaybackState.STATE_STOPPED || state == PlaybackState.STATE_PAUSED) { + mCallback.onMediaStoppedOrPaused(); + } + } + }; + + interface Callback { + /** + * Override to handle the media content updating. + */ + void onMediaChanged(); + + /** + * Override to handle the media state updating. + */ + void onMediaStoppedOrPaused(); + + /** + * Override to handle the device updating. + */ + void onRouteChanged(); + + /** + * Override to dismiss dialog. + */ + void dismissDialog(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java new file mode 100644 index 0000000000000..0dcdecfdaadb1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2020 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.systemui.media.dialog; + +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; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.RoutingSessionInfo; +import android.media.session.MediaController; +import android.media.session.MediaSessionManager; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.statusbar.phone.ShadeController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class MediaOutputControllerTest extends SysuiTestCase { + + private static final String TEST_PACKAGE_NAME = "com.test.package.name"; + 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_ARTIST = "test_artist"; + private static final String TEST_SONG = "test_song"; + private static final String TEST_SESSION_ID = "test_session_id"; + private static final String TEST_SESSION_NAME = "test_session_name"; + // Mock + private MediaController mMediaController = mock(MediaController.class); + private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class); + private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager = + mock(CachedBluetoothDeviceManager.class); + private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class); + private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class); + private MediaDevice mMediaDevice1 = mock(MediaDevice.class); + private MediaDevice mMediaDevice2 = mock(MediaDevice.class); + private MediaMetadata mMediaMetadata = mock(MediaMetadata.class); + private RoutingSessionInfo mRemoteSessionInfo = mock(RoutingSessionInfo.class); + private ShadeController mShadeController = mock(ShadeController.class); + private ActivityStarter mStarter = mock(ActivityStarter.class); + + private Context mSpyContext; + private MediaOutputController mMediaOutputController; + private LocalMediaManager mLocalMediaManager; + private List mMediaControllers = new ArrayList<>(); + private List mMediaDevices = new ArrayList<>(); + private MediaDescription mMediaDescription; + private List mRoutingSessionInfos = new ArrayList<>(); + + @Before + public void setUp() { + mSpyContext = spy(mContext); + when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGE_NAME); + mMediaControllers.add(mMediaController); + when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers); + doReturn(mMediaSessionManager).when(mSpyContext).getSystemService( + MediaSessionManager.class); + when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn( + mCachedBluetoothDeviceManager); + mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, + mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter); + mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); + mMediaOutputController.mLocalMediaManager = mLocalMediaManager; + MediaDescription.Builder builder = new MediaDescription.Builder(); + builder.setTitle(TEST_SONG); + builder.setSubtitle(TEST_ARTIST); + mMediaDescription = builder.build(); + when(mMediaMetadata.getDescription()).thenReturn(mMediaDescription); + when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_1_ID); + when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID); + mMediaDevices.add(mMediaDevice1); + mMediaDevices.add(mMediaDevice2); + } + + @Test + public void start_verifyLocalMediaManagerInit() { + mMediaOutputController.start(mCb); + + verify(mLocalMediaManager).registerCallback(mMediaOutputController); + verify(mLocalMediaManager).startScan(); + } + + @Test + public void stop_verifyLocalMediaManagerDeinit() { + mMediaOutputController.start(mCb); + reset(mLocalMediaManager); + + mMediaOutputController.stop(); + + verify(mLocalMediaManager).unregisterCallback(mMediaOutputController); + verify(mLocalMediaManager).stopScan(); + } + + @Test + public void start_withPackageName_verifyMediaControllerInit() { + mMediaOutputController.start(mCb); + + verify(mMediaController).registerCallback(any()); + } + + @Test + public void start_withoutPackageName_verifyMediaControllerInit() { + mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager, + mLocalBluetoothManager, mShadeController, mStarter); + + mMediaOutputController.start(mCb); + + verify(mMediaController, never()).registerCallback(any()); + } + + @Test + public void stop_withPackageName_verifyMediaControllerDeinit() { + mMediaOutputController.start(mCb); + reset(mMediaController); + + mMediaOutputController.stop(); + + verify(mMediaController).unregisterCallback(any()); + } + + @Test + public void stop_withoutPackageName_verifyMediaControllerDeinit() { + mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager, + mLocalBluetoothManager, mShadeController, mStarter); + mMediaOutputController.start(mCb); + + mMediaOutputController.stop(); + + verify(mMediaController, never()).unregisterCallback(any()); + } + + @Test + public void onDeviceListUpdate_verifyDeviceListCallback() { + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + final List devices = new ArrayList<>(mMediaOutputController.getMediaDevices()); + + assertThat(devices.containsAll(mMediaDevices)).isTrue(); + assertThat(devices.size()).isEqualTo(mMediaDevices.size()); + verify(mCb).onRouteChanged(); + } + + @Test + public void onSelectedDeviceStateChanged_verifyCallback() { + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onSelectedDeviceStateChanged(mMediaDevice1, + LocalMediaManager.MediaDeviceState.STATE_CONNECTED); + + verify(mCb).onRouteChanged(); + } + + @Test + public void onDeviceAttributesChanged_verifyCallback() { + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onDeviceAttributesChanged(); + + verify(mCb).onRouteChanged(); + } + + @Test + public void onRequestFailed_verifyCallback() { + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onRequestFailed(0 /* reason */); + + verify(mCb).onRouteChanged(); + } + + @Test + public void getHeaderTitle_withoutMetadata_returnDefaultString() { + when(mMediaController.getMetadata()).thenReturn(null); + + mMediaOutputController.start(mCb); + + assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo( + mContext.getText(R.string.controls_media_title)); + } + + @Test + public void getHeaderTitle_withMetadata_returnSongName() { + when(mMediaController.getMetadata()).thenReturn(mMediaMetadata); + + mMediaOutputController.start(mCb); + + assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(TEST_SONG); + } + + @Test + public void getHeaderSubTitle_withoutMetadata_returnNull() { + when(mMediaController.getMetadata()).thenReturn(null); + + mMediaOutputController.start(mCb); + + assertThat(mMediaOutputController.getHeaderSubTitle()).isNull(); + } + + @Test + public void getHeaderSubTitle_withMetadata_returnArtistName() { + when(mMediaController.getMetadata()).thenReturn(mMediaMetadata); + + mMediaOutputController.start(mCb); + + assertThat(mMediaOutputController.getHeaderSubTitle()).isEqualTo(TEST_ARTIST); + } + + @Test + public void connectDevice_verifyConnect() { + mMediaOutputController.connectDevice(mMediaDevice1); + + // Wait for background thread execution + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + verify(mLocalMediaManager).connectDevice(mMediaDevice1); + } + + @Test + public void getActiveRemoteMediaDevice_isSystemSession_returnSession() { + when(mRemoteSessionInfo.getId()).thenReturn(TEST_SESSION_ID); + when(mRemoteSessionInfo.getName()).thenReturn(TEST_SESSION_NAME); + when(mRemoteSessionInfo.getVolumeMax()).thenReturn(100); + when(mRemoteSessionInfo.getVolume()).thenReturn(10); + when(mRemoteSessionInfo.isSystemSession()).thenReturn(false); + mRoutingSessionInfos.add(mRemoteSessionInfo); + when(mLocalMediaManager.getActiveMediaSession()).thenReturn(mRoutingSessionInfos); + + assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).containsExactly( + mRemoteSessionInfo); + } + + @Test + public void getActiveRemoteMediaDevice_notSystemSession_returnEmpty() { + when(mRemoteSessionInfo.getId()).thenReturn(TEST_SESSION_ID); + when(mRemoteSessionInfo.getName()).thenReturn(TEST_SESSION_NAME); + when(mRemoteSessionInfo.getVolumeMax()).thenReturn(100); + when(mRemoteSessionInfo.getVolume()).thenReturn(10); + when(mRemoteSessionInfo.isSystemSession()).thenReturn(true); + mRoutingSessionInfos.add(mRemoteSessionInfo); + when(mLocalMediaManager.getActiveMediaSession()).thenReturn(mRoutingSessionInfos); + + assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).isEmpty(); + } + + @Test + public void isZeroMode_onlyFromPhoneOutput_returnTrue() { + // Multiple available devices + assertThat(mMediaOutputController.isZeroMode()).isFalse(); + when(mMediaDevice1.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE); + mMediaDevices.clear(); + mMediaDevices.add(mMediaDevice1); + mMediaOutputController.start(mCb); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + + assertThat(mMediaOutputController.isZeroMode()).isTrue(); + + when(mMediaDevice1.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE); + + assertThat(mMediaOutputController.isZeroMode()).isTrue(); + + when(mMediaDevice1.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE); + + assertThat(mMediaOutputController.isZeroMode()).isTrue(); + } + + @Test + public void isZeroMode_notFromPhoneOutput_returnFalse() { + when(mMediaDevice1.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_UNKNOWN); + mMediaDevices.clear(); + mMediaDevices.add(mMediaDevice1); + mMediaOutputController.start(mCb); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + + assertThat(mMediaOutputController.isZeroMode()).isFalse(); + + when(mMediaDevice1.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE); + + assertThat(mMediaOutputController.isZeroMode()).isFalse(); + + when(mMediaDevice1.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE); + + assertThat(mMediaOutputController.isZeroMode()).isFalse(); + + when(mMediaDevice1.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE); + + assertThat(mMediaOutputController.isZeroMode()).isFalse(); + + when(mMediaDevice1.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE); + + assertThat(mMediaOutputController.isZeroMode()).isFalse(); + } +} From 10ab47af3457b72bbab79c8da3779251eae07446 Mon Sep 17 00:00:00 2001 From: timhypeng Date: Mon, 7 Sep 2020 14:24:09 +0800 Subject: [PATCH 2/5] Add MediaOutputAdapter for Media device list -Build device item layout by different cases -Provide UI component for different operations -Add MediaOutputBaseAdapter for common method -Add MediaOutputAdapterTest for unit test Bug: 155822415 Test: atest MediaOutputAdapterTest Change-Id: Ida509ed401320b3665d7a5f29f4df5db096ff29c --- .../res/layout/media_output_list_item.xml | 112 ++++++++++ .../media/dialog/MediaOutputAdapter.java | 163 ++++++++++++++ .../media/dialog/MediaOutputBaseAdapter.java | 165 +++++++++++++++ .../media/dialog/MediaOutputAdapterTest.java | 200 ++++++++++++++++++ 4 files changed, 640 insertions(+) create mode 100644 packages/SystemUI/res/layout/media_output_list_item.xml create mode 100644 packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java create mode 100644 packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java diff --git a/packages/SystemUI/res/layout/media_output_list_item.xml b/packages/SystemUI/res/layout/media_output_list_item.xml new file mode 100644 index 0000000000000..92d0858a1a310 --- /dev/null +++ b/packages/SystemUI/res/layout/media_output_list_item.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java new file mode 100644 index 0000000000000..9fc64d51cdf70 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2020 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.systemui.media.dialog; + +import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; + +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import com.android.settingslib.Utils; +import com.android.settingslib.media.LocalMediaManager.MediaDeviceState; +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.R; + +import java.util.List; + +/** + * Adapter for media output dialog. + */ +public class MediaOutputAdapter extends MediaOutputBaseAdapter { + + private static final String TAG = "MediaOutputAdapter"; + private static final int PAIR_NEW = 1; + + public MediaOutputAdapter(MediaOutputController controller) { + super(controller); + } + + @Override + public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, + int viewType) { + super.onCreateViewHolder(viewGroup, viewType); + + return new MediaDeviceViewHolder(mHolderView); + } + + @Override + public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) { + if (mController.isZeroMode() && position == (mController.getMediaDevices().size())) { + viewHolder.onBind(PAIR_NEW); + } else if (position < (mController.getMediaDevices().size())) { + viewHolder.onBind(((List) (mController.getMediaDevices())).get(position)); + } else { + Log.d(TAG, "Incorrect position: " + position); + } + } + + @Override + public int getItemCount() { + if (mController.isZeroMode()) { + // Add extra one for "pair new" + return mController.getMediaDevices().size() + 1; + } + return mController.getMediaDevices().size(); + } + + void onItemClick(MediaDevice device) { + mController.connectDevice(device); + device.setState(MediaDeviceState.STATE_CONNECTING); + notifyDataSetChanged(); + } + + void onItemClick(int customizedItem) { + if (customizedItem == PAIR_NEW) { + mController.launchBluetoothPairing(); + } + } + + @Override + CharSequence getItemTitle(MediaDevice device) { + if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE + && !device.isConnected()) { + final CharSequence deviceName = device.getName(); + // Append status to title only for the disconnected Bluetooth device. + final SpannableString spannableTitle = new SpannableString( + mContext.getString(R.string.media_output_dialog_disconnected, deviceName)); + spannableTitle.setSpan(new ForegroundColorSpan( + Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorSecondary)), + deviceName.length(), + spannableTitle.length(), SPAN_EXCLUSIVE_EXCLUSIVE); + return spannableTitle; + } + return super.getItemTitle(device); + } + + class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder { + + MediaDeviceViewHolder(View view) { + super(view); + } + + @Override + void onBind(MediaDevice device) { + super.onBind(device); + if (mController.isTransferring()) { + if (device.getState() == MediaDeviceState.STATE_CONNECTING + && !mController.hasAdjustVolumeUserRestriction()) { + setTwoLineLayout(device, true); + mProgressBar.setVisibility(View.VISIBLE); + mSeekBar.setVisibility(View.GONE); + mSubTitleText.setVisibility(View.GONE); + } else { + setSingleLineLayout(getItemTitle(device), false); + } + } else { + // Set different layout for each device + if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) { + setTwoLineLayout(device, false); + mSubTitleText.setVisibility(View.VISIBLE); + mSeekBar.setVisibility(View.GONE); + mProgressBar.setVisibility(View.GONE); + mSubTitleText.setText(R.string.media_output_dialog_connect_failed); + mFrameLayout.setOnClickListener(v -> onItemClick(device)); + } else if (!mController.hasAdjustVolumeUserRestriction() + && isCurrentConnected(device)) { + setTwoLineLayout(device, true); + mSeekBar.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.GONE); + mSubTitleText.setVisibility(View.GONE); + initSeekbar(device); + } else { + setSingleLineLayout(getItemTitle(device), false); + mFrameLayout.setOnClickListener(v -> onItemClick(device)); + } + } + } + + @Override + void onBind(int customizedItem) { + if (customizedItem == PAIR_NEW) { + setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new), + false); + final Drawable d = mContext.getDrawable(R.drawable.ic_add); + d.setColorFilter(new PorterDuffColorFilter( + Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN)); + mTitleIcon.setImageDrawable(d); + mFrameLayout.setOnClickListener(v -> onItemClick(PAIR_NEW)); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java new file mode 100644 index 0000000000000..7579c25b030ab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2020 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.systemui.media.dialog; + +import android.content.Context; +import android.graphics.Typeface; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.R; + +/** + * Base adapter for media output dialog. + */ +public abstract class MediaOutputBaseAdapter extends + RecyclerView.Adapter { + + private static final String FONT_SELECTED_TITLE = "sans-serif-medium"; + private static final String FONT_TITLE = "sans-serif"; + + final MediaOutputController mController; + + private boolean mIsDragging; + + Context mContext; + View mHolderView; + + public MediaOutputBaseAdapter(MediaOutputController controller) { + mController = controller; + mIsDragging = false; + } + + @Override + public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, + int viewType) { + mContext = viewGroup.getContext(); + mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item, + viewGroup, false); + + return null; + } + + CharSequence getItemTitle(MediaDevice device) { + return device.getName(); + } + + boolean isCurrentConnected(MediaDevice device) { + return TextUtils.equals(device.getId(), + mController.getCurrentConnectedMediaDevice().getId()); + } + + boolean isDragging() { + return mIsDragging; + } + + /** + * ViewHolder for binding device view. + */ + abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder { + final FrameLayout mFrameLayout; + final TextView mTitleText; + final TextView mTwoLineTitleText; + final TextView mSubTitleText; + final ImageView mTitleIcon; + final ImageView mEndIcon; + final ProgressBar mProgressBar; + final SeekBar mSeekBar; + final RelativeLayout mTwoLineLayout; + + MediaDeviceBaseViewHolder(View view) { + super(view); + mFrameLayout = view.requireViewById(R.id.device_container); + mTitleText = view.requireViewById(R.id.title); + mSubTitleText = view.requireViewById(R.id.subtitle); + mTwoLineLayout = view.requireViewById(R.id.two_line_layout); + mTwoLineTitleText = view.requireViewById(R.id.two_line_title); + mTitleIcon = view.requireViewById(R.id.title_icon); + mEndIcon = view.requireViewById(R.id.end_icon); + mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress); + mSeekBar = view.requireViewById(R.id.volume_seekbar); + } + + void onBind(MediaDevice device) { + mTitleIcon.setImageIcon(mController.getDeviceIconCompat(device).toIcon(mContext)); + } + + void onBind(int customizedItem) { } + + void setSingleLineLayout(CharSequence title, boolean bFocused) { + mTitleText.setVisibility(View.VISIBLE); + mTwoLineLayout.setVisibility(View.GONE); + mTitleText.setText(title); + if (bFocused) { + mTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL)); + } else { + mTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL)); + } + } + + void setTwoLineLayout(MediaDevice device, boolean bFocused) { + mTitleText.setVisibility(View.GONE); + mTwoLineLayout.setVisibility(View.VISIBLE); + mTwoLineTitleText.setText(getItemTitle(device)); + if (bFocused) { + mTwoLineTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, + Typeface.NORMAL)); + } else { + mTwoLineTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL)); + } + } + + void initSeekbar(MediaDevice device) { + mSeekBar.setMax(device.getMaxVolume()); + mSeekBar.setMin(0); + if (mSeekBar.getProgress() != device.getCurrentVolume()) { + mSeekBar.setProgress(device.getCurrentVolume()); + } + mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (device == null || !fromUser) { + return; + } + mController.adjustVolume(device, progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mIsDragging = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mIsDragging = false; + } + }); + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java new file mode 100644 index 0000000000000..0e376bd356a28 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2020 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.systemui.media.dialog; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.drawable.Icon; +import android.testing.AndroidTestingRunner; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.test.filters.SmallTest; + +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class MediaOutputAdapterTest extends SysuiTestCase { + + private static final String TEST_DEVICE_NAME_1 = "test_device_name_1"; + private static final String TEST_DEVICE_NAME_2 = "test_device_name_2"; + private static final String TEST_DEVICE_ID_1 = "test_device_id_1"; + private static final String TEST_DEVICE_ID_2 = "test_device_id_2"; + + // Mock + private MediaOutputController mMediaOutputController = mock(MediaOutputController.class); + private MediaDevice mMediaDevice1 = mock(MediaDevice.class); + private MediaDevice mMediaDevice2 = mock(MediaDevice.class); + private Icon mIcon = mock(Icon.class); + private IconCompat mIconCompat = mock(IconCompat.class); + + private MediaOutputAdapter mMediaOutputAdapter; + private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder; + private List mMediaDevices = new ArrayList<>(); + + @Before + public void setUp() { + mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); + mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + .onCreateViewHolder(new FrameLayout(mContext), 0); + + when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices); + when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false); + when(mMediaOutputController.isZeroMode()).thenReturn(false); + when(mMediaOutputController.isTransferring()).thenReturn(false); + when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat); + when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat); + when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1); + when(mIconCompat.toIcon(mContext)).thenReturn(mIcon); + when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1); + when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1); + when(mMediaDevice2.getName()).thenReturn(TEST_DEVICE_NAME_2); + when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_ID_2); + when(mMediaDevice1.getState()).thenReturn( + LocalMediaManager.MediaDeviceState.STATE_CONNECTED); + when(mMediaDevice2.getState()).thenReturn( + LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); + mMediaDevices.add(mMediaDevice1); + mMediaDevices.add(mMediaDevice2); + } + + @Test + public void getItemCount_nonZeroMode_isDeviceSize() { + assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size()); + } + + @Test + public void getItemCount_zeroMode_containExtraOneForPairNew() { + when(mMediaOutputController.isZeroMode()).thenReturn(true); + + assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size() + 1); + } + + @Test + public void onBindViewHolder_zeroMode_bindPairNew_verifyView() { + when(mMediaOutputController.isZeroMode()).thenReturn(true); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2); + + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mTitleText.getText()).isEqualTo(mContext.getText( + R.string.media_output_dialog_pairing_new)); + } + + @Test + public void onBindViewHolder_bindConnectedDevice_verifyView() { + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); + + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1); + } + + @Test + public void onBindViewHolder_bindDisconnectedBluetoothDevice_verifyView() { + when(mMediaDevice2.getDeviceType()).thenReturn( + MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE); + when(mMediaDevice2.isConnected()).thenReturn(false); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + + assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo( + mContext.getString(R.string.media_output_dialog_disconnected, TEST_DEVICE_NAME_2)); + } + + @Test + public void onBindViewHolder_bindFailedStateDevice_verifyView() { + when(mMediaDevice2.getState()).thenReturn( + LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mSubTitleText.getText()).isEqualTo(mContext.getText( + R.string.media_output_dialog_connect_failed)); + assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2); + } + + @Test + public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() { + when(mMediaOutputController.isTransferring()).thenReturn(true); + when(mMediaDevice1.getState()).thenReturn( + LocalMediaManager.MediaDeviceState.STATE_CONNECTING); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); + + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1); + } + + @Test + public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() { + when(mMediaOutputController.isTransferring()).thenReturn(true); + when(mMediaDevice2.getState()).thenReturn( + LocalMediaManager.MediaDeviceState.STATE_CONNECTING); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); + + assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE); + assertThat(mViewHolder.mTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1); + } + + @Test + public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() { + when(mMediaOutputController.isZeroMode()).thenReturn(true); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2); + mViewHolder.mFrameLayout.performClick(); + + verify(mMediaOutputController).launchBluetoothPairing(); + } + + @Test + public void onItemClick_clickDevice_verifyConnectDevice() { + assertThat(mMediaDevice2.getState()).isEqualTo( + LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); + mViewHolder.mFrameLayout.performClick(); + + verify(mMediaOutputController).connectDevice(mMediaDevice2); + } +} From eef372d1b20dbf3f2e0fcc1b04cf6159e068dd03 Mon Sep 17 00:00:00 2001 From: timhypeng Date: Tue, 8 Sep 2020 16:29:53 +0800 Subject: [PATCH 3/5] Add Media Output Dialog for Output Switcher -Add MediaOutputBaseDialog to provide common method for different media operations UI -Add MediaOutputDialog for showing Bluetooth device -Add resources for background image, style and layout -Add MediaOutputBaseDialogTest for unit test Bug: 155822415 Test: atest MediaOutputBaseDialogTest Change-Id: I3086a4049f240870ca1ad870946d6848e500b561 --- .../media_output_dialog_background.xml | 23 ++ .../res/layout/media_output_dialog.xml | 136 +++++++++++ packages/SystemUI/res/values/styles.xml | 5 +- .../media/dialog/MediaOutputBaseDialog.java | 220 ++++++++++++++++++ .../media/dialog/MediaOutputDialog.java | 77 ++++++ .../dialog/MediaOutputBaseDialogTest.java | 212 +++++++++++++++++ 6 files changed, 672 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/media_output_dialog_background.xml create mode 100644 packages/SystemUI/res/layout/media_output_dialog.xml create mode 100644 packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java create mode 100644 packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_background.xml new file mode 100644 index 0000000000000..3ceb0f6ac06ae --- /dev/null +++ b/packages/SystemUI/res/drawable/media_output_dialog_background.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml new file mode 100644 index 0000000000000..0229e6e9d4dd3 --- /dev/null +++ b/packages/SystemUI/res/layout/media_output_dialog.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +