diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index 415870c590a35..b1dfbb575ad7b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -21,7 +21,7 @@ import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY; import android.app.Dialog; import android.content.Context; import android.content.Intent; -import android.media.projection.MediaProjectionInfo; +import android.media.MediaRouter.RouteInfo; import android.provider.Settings; import android.service.quicksettings.Tile; import android.util.Log; @@ -48,8 +48,9 @@ import com.android.systemui.statusbar.policy.CastController.CastDevice; import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.NetworkController; +import java.util.ArrayList; import java.util.LinkedHashMap; -import java.util.Set; +import java.util.List; import javax.inject.Inject; @@ -128,35 +129,30 @@ public class CastTile extends QSTileImpl { return; } - CastDevice activeProjection = getActiveDeviceMediaProjection(); - if (activeProjection == null) { - if (mKeyguard.isSecure() && !mKeyguard.canSkipBouncer()) { - mActivityStarter.postQSRunnableDismissingKeyguard(() -> { - showDetail(true); - }); - } else { + List activeDevices = getActiveDevices(); + // We want to pop up the media route selection dialog if we either have no active devices + // (neither routes nor projection), or if we have an active route. In other cases, we assume + // that a projection is active. This is messy, but this tile never correctly handled the + // case where multiple devices were active :-/. + if (activeDevices.isEmpty() || (activeDevices.get(0).tag instanceof RouteInfo)) { + mActivityStarter.postQSRunnableDismissingKeyguard(() -> { showDetail(true); - } + }); } else { - mController.stopCasting(activeProjection); + mController.stopCasting(activeDevices.get(0)); } } - private CastDevice getActiveDeviceMediaProjection() { - CastDevice activeDevice = null; + private List getActiveDevices() { + ArrayList activeDevices = new ArrayList<>(); for (CastDevice device : mController.getCastDevices()) { if (device.state == CastDevice.STATE_CONNECTED || device.state == CastDevice.STATE_CONNECTING) { - activeDevice = device; - break; + activeDevices.add(device); } } - if (activeDevice != null && activeDevice.tag instanceof MediaProjectionInfo) { - return activeDevice; - } - - return null; + return activeDevices; } @Override @@ -187,14 +183,18 @@ public class CastTile extends QSTileImpl { state.label = mContext.getString(R.string.quick_settings_cast_title); state.contentDescription = state.label; state.value = false; - final Set devices = mController.getCastDevices(); + final List devices = mController.getCastDevices(); boolean connecting = false; + // We always choose the first device that's in the CONNECTED state in the case where + // multiple devices are CONNECTED at the same time. for (CastDevice device : devices) { if (device.state == CastDevice.STATE_CONNECTED) { state.value = true; state.secondaryLabel = getDeviceName(device); state.contentDescription = state.contentDescription + "," + mContext.getString(R.string.accessibility_cast_name, state.label); + connecting = false; + break; } else if (device.state == CastDevice.STATE_CONNECTING) { connecting = true; } @@ -326,7 +326,7 @@ public class CastTile extends QSTileImpl { return mItems; } - private void updateItems(Set devices) { + private void updateItems(List devices) { if (mItems == null) return; Item[] items = null; if (devices != null && !devices.isEmpty()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java index 97be6ed10bf90..98cde2a049daf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java @@ -19,12 +19,12 @@ package com.android.systemui.statusbar.policy; import com.android.systemui.Dumpable; import com.android.systemui.statusbar.policy.CastController.Callback; -import java.util.Set; +import java.util.List; public interface CastController extends CallbackController, Dumpable { void setDiscovering(boolean request); void setCurrentUserId(int currentUserId); - Set getCastDevices(); + List getCastDevices(); void startCasting(CastDevice device); void stopCasting(CastDevice device); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java index c7d337ad46d6b..505dd169839e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java @@ -29,7 +29,6 @@ import android.media.projection.MediaProjectionManager; import android.os.Handler; import android.text.TextUtils; import android.util.ArrayMap; -import android.util.ArraySet; import android.util.Log; import androidx.annotation.VisibleForTesting; @@ -40,8 +39,8 @@ import com.android.systemui.R; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.UUID; import javax.inject.Inject; @@ -150,8 +149,31 @@ public class CastControllerImpl implements CastController { } @Override - public Set getCastDevices() { - final ArraySet devices = new ArraySet(); + public List getCastDevices() { + final ArrayList devices = new ArrayList<>(); + synchronized(mRoutes) { + for (RouteInfo route : mRoutes.values()) { + final CastDevice device = new CastDevice(); + device.id = route.getTag().toString(); + final CharSequence name = route.getName(mContext); + device.name = name != null ? name.toString() : null; + final CharSequence description = route.getDescription(); + device.description = description != null ? description.toString() : null; + + int statusCode = route.getStatusCode(); + if (statusCode == RouteInfo.STATUS_CONNECTING) { + device.state = CastDevice.STATE_CONNECTING; + } else if (route.isSelected() || statusCode == RouteInfo.STATUS_CONNECTED) { + device.state = CastDevice.STATE_CONNECTED; + } else { + device.state = CastDevice.STATE_DISCONNECTED; + } + + device.tag = route; + devices.add(device); + } + } + synchronized (mProjectionLock) { if (mProjection != null) { final CastDevice device = new CastDevice(); @@ -161,24 +183,9 @@ public class CastControllerImpl implements CastController { device.state = CastDevice.STATE_CONNECTED; device.tag = mProjection; devices.add(device); - return devices; - } - } - synchronized(mRoutes) { - for (RouteInfo route : mRoutes.values()) { - final CastDevice device = new CastDevice(); - device.id = route.getTag().toString(); - final CharSequence name = route.getName(mContext); - device.name = name != null ? name.toString() : null; - final CharSequence description = route.getDescription(); - device.description = description != null ? description.toString() : null; - device.state = route.isConnecting() ? CastDevice.STATE_CONNECTING - : route.isSelected() ? CastDevice.STATE_CONNECTED - : CastDevice.STATE_DISCONNECTED; - device.tag = route; - devices.add(device); } } + return devices; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java index ea8d4b2f8f60d..818db878cdc0a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java @@ -14,13 +14,19 @@ package com.android.systemui.qs.tiles; +import static junit.framework.Assert.assertTrue; import static junit.framework.TestCase.assertEquals; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.media.MediaRouter; +import android.media.MediaRouter.RouteInfo; +import android.media.projection.MediaProjectionInfo; import android.service.quicksettings.Tile; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -33,6 +39,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSTileHost; import com.android.systemui.statusbar.policy.CastController; +import com.android.systemui.statusbar.policy.CastController.CastDevice; import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.NetworkController; @@ -43,8 +50,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; + @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @@ -93,7 +101,6 @@ public class CastTileTest extends SysuiTestCase { verify(mNetworkController).observe(any(LifecycleOwner.class), signalCallbackArgumentCaptor.capture()); mCallback = signalCallbackArgumentCaptor.getValue(); - } @Test @@ -120,33 +127,125 @@ public class CastTileTest extends SysuiTestCase { assertEquals(Tile.STATE_UNAVAILABLE, mCastTile.getState().state); } - @Test - public void testStateActive_wifiEnabledAndCasting() { - CastController.CastDevice device = mock(CastController.CastDevice.class); - device.state = CastController.CastDevice.STATE_CONNECTED; - Set devices = new HashSet<>(); - devices.add(device); - when(mController.getCastDevices()).thenReturn(devices); - + private void enableWifiAndProcessMessages() { NetworkController.IconState qsIcon = new NetworkController.IconState(true, 0, ""); mCallback.setWifiIndicators(true, mock(NetworkController.IconState.class), qsIcon, false,false, "", false, ""); mTestableLooper.processAllMessages(); + } + @Test + public void testStateActive_wifiEnabledAndCasting() { + CastController.CastDevice device = new CastController.CastDevice(); + device.state = CastController.CastDevice.STATE_CONNECTED; + List devices = new ArrayList<>(); + devices.add(device); + when(mController.getCastDevices()).thenReturn(devices); + + enableWifiAndProcessMessages(); assertEquals(Tile.STATE_ACTIVE, mCastTile.getState().state); } @Test public void testStateInactive_wifiEnabledNotCasting() { - NetworkController.IconState qsIcon = - new NetworkController.IconState(true, 0, ""); - mCallback.setWifiIndicators(true, mock(NetworkController.IconState.class), - qsIcon, false,false, "", - false, ""); - mTestableLooper.processAllMessages(); - + enableWifiAndProcessMessages(); assertEquals(Tile.STATE_INACTIVE, mCastTile.getState().state); } + + @Test + public void testHandleClick_castDevicePresent() { + CastController.CastDevice device = new CastController.CastDevice(); + device.state = CastDevice.STATE_CONNECTED; + device.tag = mock(MediaRouter.RouteInfo.class); + List devices = new ArrayList<>(); + devices.add(device); + when(mController.getCastDevices()).thenReturn(devices); + + enableWifiAndProcessMessages(); + mCastTile.handleClick(); + mTestableLooper.processAllMessages(); + + verify(mActivityStarter, times(1)).postQSRunnableDismissingKeyguard(any()); + } + + @Test + public void testHandleClick_projectionOnly() { + CastController.CastDevice device = new CastController.CastDevice(); + device.state = CastDevice.STATE_CONNECTED; + device.tag = mock(MediaProjectionInfo.class); + List devices = new ArrayList<>(); + devices.add(device); + when(mController.getCastDevices()).thenReturn(devices); + + enableWifiAndProcessMessages(); + mCastTile.handleClick(); + mTestableLooper.processAllMessages(); + + verify(mController, times(1)).stopCasting(same(device)); + } + + @Test + public void testUpdateState_projectionOnly() { + CastController.CastDevice device = new CastController.CastDevice(); + device.state = CastDevice.STATE_CONNECTED; + device.tag = mock(MediaProjectionInfo.class); + device.name = "Test Projection Device"; + List devices = new ArrayList<>(); + devices.add(device); + when(mController.getCastDevices()).thenReturn(devices); + + enableWifiAndProcessMessages(); + assertEquals(Tile.STATE_ACTIVE, mCastTile.getState().state); + assertTrue(mCastTile.getState().secondaryLabel.toString().startsWith(device.name)); + } + + @Test + public void testUpdateState_castingAndProjection() { + CastController.CastDevice casting = new CastController.CastDevice(); + casting.state = CastDevice.STATE_CONNECTED; + casting.tag = mock(RouteInfo.class); + casting.name = "Test Casting Device"; + + CastController.CastDevice projection = new CastController.CastDevice(); + projection.state = CastDevice.STATE_CONNECTED; + projection.tag = mock(MediaProjectionInfo.class); + projection.name = "Test Projection Device"; + + List devices = new ArrayList<>(); + devices.add(casting); + devices.add(projection); + when(mController.getCastDevices()).thenReturn(devices); + + enableWifiAndProcessMessages(); + + // Note here that the tile should be active, and should choose casting over projection. + assertEquals(Tile.STATE_ACTIVE, mCastTile.getState().state); + assertTrue(mCastTile.getState().secondaryLabel.toString().startsWith(casting.name)); + } + + @Test + public void testUpdateState_connectedAndConnecting() { + CastController.CastDevice connecting = new CastController.CastDevice(); + connecting.state = CastDevice.STATE_CONNECTING; + connecting.tag = mock(RouteInfo.class); + connecting.name = "Test Casting Device"; + + CastController.CastDevice connected = new CastController.CastDevice(); + connected.state = CastDevice.STATE_CONNECTED; + connected.tag = mock(RouteInfo.class); + connected.name = "Test Casting Device"; + + List devices = new ArrayList<>(); + devices.add(connecting); + devices.add(connected); + when(mController.getCastDevices()).thenReturn(devices); + + enableWifiAndProcessMessages(); + + // Tile should be connected and always prefer the connected device. + assertEquals(Tile.STATE_ACTIVE, mCastTile.getState().state); + assertTrue(mCastTile.getState().secondaryLabel.toString().startsWith(connected.name)); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java index 1248cbbd335d0..67ad37b2d29a8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java @@ -45,7 +45,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Collections; -import java.util.Set; +import java.util.List; @RunWith(AndroidTestingRunner.class) @RunWithLooper @@ -118,10 +118,10 @@ public class AutoTileManagerTest extends SysuiTestCase { verify(mQsTileHost, never()).addTile("night"); } - private static Set buildFakeCastDevice(boolean isCasting) { + private static List buildFakeCastDevice(boolean isCasting) { CastDevice cd = new CastDevice(); cd.state = isCasting ? CastDevice.STATE_CONNECTED : CastDevice.STATE_DISCONNECTED; - return Collections.singleton(cd); + return Collections.singletonList(cd); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeCastController.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeCastController.java index 51149abe792d3..f6b24da9b821b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeCastController.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeCastController.java @@ -19,7 +19,8 @@ import android.testing.LeakCheck; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.CastController.Callback; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; public class FakeCastController extends BaseLeakChecker implements CastController { public FakeCastController(LeakCheck test) { @@ -37,8 +38,8 @@ public class FakeCastController extends BaseLeakChecker implements Cas } @Override - public Set getCastDevices() { - return null; + public List getCastDevices() { + return new ArrayList<>(); } @Override