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:
Narayan Kamath
2019-03-21 18:09:38 +00:00
parent 94fda28a6b
commit 32492eebf9
6 changed files with 175 additions and 68 deletions

View File

@@ -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()) {

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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));
}
}

View File

@@ -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

View File

@@ -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