Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 32492eeb authored by Narayan Kamath's avatar Narayan Kamath
Browse files

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
parent 94fda28a
Loading
Loading
Loading
Loading
+22 −22
Original line number Diff line number Diff line
@@ -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()) {
        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 {
                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()) {
+2 −2
Original line number Diff line number Diff line
@@ -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);

+26 −19
Original line number Diff line number Diff line
@@ -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,20 +149,8 @@ public class CastControllerImpl implements CastController {
    }

    @Override
    public Set<CastDevice> getCastDevices() {
        final ArraySet<CastDevice> devices = new ArraySet<CastDevice>();
        synchronized (mProjectionLock) {
            if (mProjection != null) {
                final CastDevice device = new CastDevice();
                device.id = mProjection.getPackageName();
                device.name = getAppName(mProjection.getPackageName());
                device.description = mContext.getString(R.string.quick_settings_casting);
                device.state = CastDevice.STATE_CONNECTED;
                device.tag = mProjection;
                devices.add(device);
                return devices;
            }
        }
    public List<CastDevice> getCastDevices() {
        final ArrayList<CastDevice> devices = new ArrayList<>();
        synchronized(mRoutes) {
            for (RouteInfo route : mRoutes.values()) {
                final CastDevice device = new CastDevice();
@@ -172,13 +159,33 @@ public class CastControllerImpl implements CastController {
                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;

                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();
                device.id = mProjection.getPackageName();
                device.name = getAppName(mProjection.getPackageName());
                device.description = mContext.getString(R.string.quick_settings_casting);
                device.state = CastDevice.STATE_CONNECTED;
                device.tag = mProjection;
                devices.add(device);
            }
        }

        return devices;
    }

+116 −17
Original line number Diff line number Diff line
@@ -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, "");
        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();

        assertEquals(Tile.STATE_INACTIVE, mCastTile.getState().state);
        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));
    }
}
+3 −3
Original line number Diff line number Diff line
@@ -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
Loading