CastTile: Better handling of multiple active devices.
With the new MediaProjection based flow for cast, we will have a connected MediaRoute active at the same time as a MediaProjection session. In order to deal with them correctly, we need to assume in a few places that we have more than one active CastDevice. We also consider all devices that are connected, regardless of whether the given route is selected or not. Test: Manual Test: atest CastControllerImplTest Test: atest SystemUITests Bug: 128515798 Change-Id: Ie46798633f69c347ee32e0799d6cb23576122dd9
This commit is contained in:
@@ -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<BooleanState> {
|
||||
return;
|
||||
}
|
||||
|
||||
CastDevice activeProjection = getActiveDeviceMediaProjection();
|
||||
if (activeProjection == null) {
|
||||
if (mKeyguard.isSecure() && !mKeyguard.canSkipBouncer()) {
|
||||
mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
|
||||
showDetail(true);
|
||||
});
|
||||
} else {
|
||||
List<CastDevice> 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<CastDevice> getActiveDevices() {
|
||||
ArrayList<CastDevice> 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<BooleanState> {
|
||||
state.label = mContext.getString(R.string.quick_settings_cast_title);
|
||||
state.contentDescription = state.label;
|
||||
state.value = false;
|
||||
final Set<CastDevice> devices = mController.getCastDevices();
|
||||
final List<CastDevice> 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<BooleanState> {
|
||||
return mItems;
|
||||
}
|
||||
|
||||
private void updateItems(Set<CastDevice> devices) {
|
||||
private void updateItems(List<CastDevice> devices) {
|
||||
if (mItems == null) return;
|
||||
Item[] items = null;
|
||||
if (devices != null && !devices.isEmpty()) {
|
||||
|
||||
@@ -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<Callback>, Dumpable {
|
||||
void setDiscovering(boolean request);
|
||||
void setCurrentUserId(int currentUserId);
|
||||
Set<CastDevice> getCastDevices();
|
||||
List<CastDevice> getCastDevices();
|
||||
void startCasting(CastDevice device);
|
||||
void stopCasting(CastDevice device);
|
||||
|
||||
|
||||
@@ -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<CastDevice> getCastDevices() {
|
||||
final ArraySet<CastDevice> devices = new ArraySet<CastDevice>();
|
||||
public List<CastDevice> getCastDevices() {
|
||||
final ArrayList<CastDevice> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<CastController.CastDevice> 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<CastDevice> 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<CastDevice> 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<CastDevice> 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<CastDevice> 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<CastDevice> 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<CastDevice> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CastDevice> buildFakeCastDevice(boolean isCasting) {
|
||||
private static List<CastDevice> 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
|
||||
|
||||
@@ -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<Callback> implements CastController {
|
||||
public FakeCastController(LeakCheck test) {
|
||||
@@ -37,8 +38,8 @@ public class FakeCastController extends BaseLeakChecker<Callback> implements Cas
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<CastDevice> getCastDevices() {
|
||||
return null;
|
||||
public List<CastDevice> getCastDevices() {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user