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

Commit 5e2cf5f5 authored by Sarah Chin's avatar Sarah Chin Committed by Gerrit Code Review
Browse files

Merge "Update logic for enterprise connections"

parents 358394b2 f249cbc5
Loading
Loading
Loading
Loading
+74 −22
Original line number Diff line number Diff line
@@ -563,7 +563,9 @@ public class DataConnection extends StateMachine {
        ERROR_RADIO_NOT_AVAILABLE,
        ERROR_INVALID_ARG,
        ERROR_STALE,
        ERROR_DATA_SERVICE_SPECIFIC_ERROR;
        ERROR_DATA_SERVICE_SPECIFIC_ERROR,
        ERROR_DUPLICATE_CID,
        ERROR_NO_DEFAULT_CONNECTION;

        public int mFailCause;

@@ -636,7 +638,7 @@ public class DataConnection extends StateMachine {
    public void updateQosParameters(final @Nullable DataCallResponse response) {
        if (response == null) {
            mDefaultQos = null;
            mQosBearerSessions = null;
            mQosBearerSessions.clear();
            return;
        }

@@ -879,7 +881,7 @@ public class DataConnection extends StateMachine {
        String dnn = null;
        String osAppId = null;
        if (cp.mApnContext.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) {
            // TODO: update osAppId to use NetworkCapability API once it's available
            // TODO(b/181916712): update osAppId to use NetworkCapability API once it's available
            osAppId = ApnSetting.getApnTypesStringFromBitmask(mApnSetting.getApnTypeBitmask());
        } else {
            dnn = mApnSetting.getApnName();
@@ -1308,6 +1310,7 @@ public class DataConnection extends StateMachine {
        mApnContexts.clear();
        mApnSetting = null;
        mUnmeteredUseOnly = false;
        mEnterpriseUse = false;
        mRestrictedNetworkOverride = false;
        mDcFailCause = DataFailCause.NONE;
        mDisabledApnTypeBitMask = 0;
@@ -1319,6 +1322,10 @@ public class DataConnection extends StateMachine {
        mIsSuspended = false;
        mHandoverState = HANDOVER_STATE_IDLE;
        mHandoverFailureMode = DataCallResponse.HANDOVER_FAILURE_MODE_UNKNOWN;
        mSliceInfo = null;
        mDefaultQos = null;
        mQosBearerSessions.clear();
        mTrafficDescriptors.clear();
    }

    /**
@@ -1351,6 +1358,14 @@ public class DataConnection extends StateMachine {
                result = SetupResult.ERROR_DATA_SERVICE_SPECIFIC_ERROR;
                result.mFailCause = DataFailCause.getFailCause(response.getCause());
            }
        } else if (cp.mApnContext.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE
                && mDcController.getActiveDcByCid(response.getId()) != null) {
            if (DBG) log("DataConnection already exists for cid: " + response.getId());
            result = SetupResult.ERROR_DUPLICATE_CID;
        } else if (cp.mApnContext.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE
                && !mDcController.isDefaultDataActive()) {
            if (DBG) log("No default data connection currently active");
            result = SetupResult.ERROR_NO_DEFAULT_CONNECTION;
        } else {
            if (DBG) log("onSetupConnectionCompleted received successful DataCallResponse");
            mCid = response.getId();
@@ -1615,6 +1630,14 @@ public class DataConnection extends StateMachine {
     */
    private boolean mRestrictedNetworkOverride = false;

    /**
     * Indicates if this data connection supports enterprise use. Note that this flag should be
     * populated when data becomes active. Once it is set, the value cannot be changed because
     * setting it will cause this data connection to lose immutable network capabilities, which can
     * cause issues in connectivity service.
     */
    private boolean mEnterpriseUse = false;

    /**
     * Check if this data connection should be restricted. We should call this when data connection
     * becomes active, or when we want to re-evaluate the conditions to decide if we need to
@@ -1622,7 +1645,6 @@ public class DataConnection extends StateMachine {
     *
     * @return True if this data connection needs to be restricted.
     */

    private boolean shouldRestrictNetwork() {
        // first, check if there is any network request that containing restricted capability
        // (i.e. Do not have NET_CAPABILITY_NOT_RESTRICTED in the request)
@@ -1692,6 +1714,25 @@ public class DataConnection extends StateMachine {
        return true;
    }

    /**
     * Check if this data connection supports enterprise use. We call this when the data connection
     * becomes active or when we want to reevaluate the conditions to decide if we need to update
     * the network agent capabilities.
     *
     * @return True if this data connection supports enterprise use.
     */
    private boolean isEnterpriseUse() {
        // TODO(b/181916712): update osAppId to use NetworkCapability API once it's available
        boolean enterpriseTrafficDescriptor = mTrafficDescriptors
                .stream()
                .anyMatch(td -> td.getOsAppId() != null && td.getOsAppId().equals(
                        ApnSetting.TYPE_ENTERPRISE_STRING));
        boolean enterpriseApnContext = mApnContexts.keySet()
                .stream()
                .anyMatch(ac -> ac.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE);
        return enterpriseTrafficDescriptor || enterpriseApnContext;
    }

    /**
     * Get the network capabilities for this data connection.
     *
@@ -1701,8 +1742,9 @@ public class DataConnection extends StateMachine {
        final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
        boolean hasInternet = false;
        boolean unmeteredApns = false;

        if (mApnSetting != null) {
        if (mApnSetting != null && !mEnterpriseUse) {
            final String[] types = ApnSetting.getApnTypesStringFromBitmask(
                    mApnSetting.getApnTypeBitmask() & ~mDisabledApnTypeBitMask).split(",");
            for (String type : types) {
@@ -1776,11 +1818,15 @@ public class DataConnection extends StateMachine {
                builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
            }

            if (!ApnSettingUtils.isMetered(mApnSetting, mPhone)) {
                unmeteredApns = true;
            }
        }

        // Mark NOT_METERED in the following cases:
        // 1. All APNs in the APN settings are unmetered.
        // 2. The non-restricted data is intended for unmetered use only.
            if ((mUnmeteredUseOnly && !mRestrictedNetworkOverride)
                    || !ApnSettingUtils.isMetered(mApnSetting, mPhone)) {
        if (unmeteredApns || (mUnmeteredUseOnly && !mRestrictedNetworkOverride)) {
            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
        } else {
            builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
@@ -1790,13 +1836,10 @@ public class DataConnection extends StateMachine {
        if (builder.build().deduceRestrictedCapability()) {
            builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
        }
        }

        for (ApnContext ctx : mApnContexts.keySet()) {
            if (ctx.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) {
        if (mEnterpriseUse) {
            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_ENTERPRISE);
        }
        }

        if (mRestrictedNetworkOverride) {
            builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
@@ -2529,6 +2572,12 @@ public class DataConnection extends StateMachine {
                                    DataCallResponse.HANDOVER_FAILURE_MODE_UNKNOWN);
                            transitionTo(mInactiveState);
                            break;
                        case ERROR_DUPLICATE_CID:
                            // TODO (b/180988471): Properly handle the case when an existing cid is
                            // returned by tearing down the network agent if enterprise changed.
                        case ERROR_NO_DEFAULT_CONNECTION:
                            // TODO (b/180988471): Properly handle the case when a default data
                            // connection doesn't exist.
                        case ERROR_INVALID_ARG:
                            // The addresses given from the RIL are bad
                            tearDownData(cp);
@@ -2659,10 +2708,12 @@ public class DataConnection extends StateMachine {
            }

            mUnmeteredUseOnly = isUnmeteredUseOnly();
            mEnterpriseUse = isEnterpriseUse();

            if (DBG) {
                log("mRestrictedNetworkOverride = " + mRestrictedNetworkOverride
                        + ", mUnmeteredUseOnly = " + mUnmeteredUseOnly);
                        + ", mUnmeteredUseOnly = " + mUnmeteredUseOnly
                        + ", mEnterpriseUse = " + mEnterpriseUse);
            }

            // Always register a VcnNetworkPolicyChangeListener, regardless of whether this is a
@@ -3773,6 +3824,7 @@ public class DataConnection extends StateMachine {
        pw.println("mUserData=" + mUserData);
        pw.println("mRestrictedNetworkOverride=" + mRestrictedNetworkOverride);
        pw.println("mUnmeteredUseOnly=" + mUnmeteredUseOnly);
        pw.println("mEnterpriseUse=" + mEnterpriseUse);
        pw.println("mUnmeteredOverride=" + mUnmeteredOverride);
        pw.println("mCongestedOverride=" + mCongestedOverride);
        pw.println("mDownlinkBandwidth" + mDownlinkBandwidth);
+9 −0
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.os.Message;
import android.os.RegistrantList;
import android.telephony.AccessNetworkConstants;
import android.telephony.DataFailCause;
import android.telephony.data.ApnSetting;
import android.telephony.data.DataCallResponse;

import com.android.internal.telephony.DctConstants;
@@ -161,6 +162,14 @@ public class DcController extends Handler {
        }
    }

    boolean isDefaultDataActive() {
        synchronized (mDcListAll) {
            return mDcListActiveByCid.values().stream()
                    .anyMatch(dc -> dc.getApnContexts().stream()
                            .anyMatch(apn -> apn.getApnTypeBitmask() == ApnSetting.TYPE_DEFAULT));
        }
    }

    @Override
    public void handleMessage(Message msg) {
        AsyncResult ar;
+0 −9
Original line number Diff line number Diff line
@@ -47,7 +47,6 @@ import android.telephony.ServiceState;
import android.telephony.SignalStrength;
import android.telephony.SignalThresholdInfo;
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
import android.telephony.data.DataCallResponse;
import android.telephony.data.DataProfile;
import android.telephony.data.SliceInfo;
@@ -1216,14 +1215,6 @@ public class SimulatedCommands extends BaseCommands
            }
        }

        // Store different cids to simulate concurrent IMS and default data calls
        if ((dataProfile.getSupportedApnTypesBitmask() & ApnSetting.TYPE_IMS)
            == ApnSetting.TYPE_IMS) {
            mSetupDataCallResult.cid = 0;
        } else {
            mSetupDataCallResult.cid = 1;
        }

        DataCallResponse response = RIL.convertDataCallResult(mSetupDataCallResult);
        if (mDcSuccess) {
            resultSuccess(result, response);
+153 −17
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import static org.mockito.Mockito.verify;

import android.content.IntentFilter;
import android.content.pm.ServiceInfo;
import android.hardware.radio.V1_0.SetupDataCallResult;
import android.net.InetAddresses;
import android.net.KeepalivePacketData;
import android.net.LinkAddress;
@@ -86,6 +87,7 @@ import java.util.Arrays;
import java.util.Collections;

public class DataConnectionTest extends TelephonyTest {
    private static final int DEFAULT_DC_CID = 10;

    @Mock
    DcTesterFailBringUpAll mDcTesterFailBringUpAll;
@@ -96,9 +98,13 @@ public class DataConnectionTest extends TelephonyTest {
    @Mock
    ApnContext mApnContext;
    @Mock
    ApnContext mEnterpriseApnContext;
    @Mock
    DcFailBringUp mDcFailBringUp;
    @Mock
    DataCallSessionStats mDataCallSessionStats;
    @Mock
    DataConnection mDefaultDc;

    private DataConnection mDc;
    private DataConnectionTestHandler mDataConnectionTestHandler;
@@ -290,6 +296,7 @@ public class DataConnectionTest extends TelephonyTest {
                ServiceState.RIL_RADIO_TECHNOLOGY_UMTS);
        doReturn(mApn1).when(mApnContext).getApnSetting();
        doReturn(ApnSetting.TYPE_DEFAULT_STRING).when(mApnContext).getApnType();
        doReturn(ApnSetting.TYPE_DEFAULT).when(mApnContext).getApnTypeBitmask();

        mDcFailBringUp.saveParameters(0, 0, -2);
        doReturn(mDcFailBringUp).when(mDcTesterFailBringUpAll).getDcFailBringUp();
@@ -353,8 +360,13 @@ public class DataConnectionTest extends TelephonyTest {
        return (boolean) method.invoke(mDc);
    }

    private SetupResult setLinkProperties(DataCallResponse response,
                                                         LinkProperties linkProperties)
    private boolean isEnterpriseUse() throws Exception {
        Method method = DataConnection.class.getDeclaredMethod("isEnterpriseUse");
        method.setAccessible(true);
        return (boolean) method.invoke(mDc);
    }

    private SetupResult setLinkProperties(DataCallResponse response, LinkProperties linkProperties)
            throws Exception {
        Class[] cArgs = new Class[2];
        cArgs[0] = DataCallResponse.class;
@@ -374,9 +386,7 @@ public class DataConnectionTest extends TelephonyTest {
    @SmallTest
    public void testConnectEvent() throws Exception {
        testSanity();

        mDc.sendMessage(DataConnection.EVENT_CONNECT, mCp);
        waitForMs(200);
        connectEvent(true);

        verify(mCT, times(1)).registerForVoiceCallStarted(any(Handler.class),
                eq(DataConnection.EVENT_DATA_CONNECTION_VOICE_CALL_STARTED), eq(null));
@@ -417,7 +427,7 @@ public class DataConnectionTest extends TelephonyTest {
                assertEquals(null, tdCaptor.getValue().getOsAppId());
            }
        }
        assertEquals("DcActiveState", getCurrentState().getName());
        assertTrue(mDc.isActive());

        assertEquals(mDc.getPduSessionId(), 1);
        assertEquals(3, mDc.getPcscfAddresses().length);
@@ -426,14 +436,86 @@ public class DataConnectionTest extends TelephonyTest {
        assertTrue(Arrays.stream(mDc.getPcscfAddresses()).anyMatch("fd00:976a:c305:1d::5"::equals));
    }

    @Test
    public void testConnectEventDuplicateContextIds() throws Exception {
        setUpDefaultData();

        // Create successful result with the same CID as default
        SetupDataCallResult result = new SetupDataCallResult();
        result.status = 0;
        result.suggestedRetryTime = -1;
        result.cid = DEFAULT_DC_CID;
        result.active = 2;
        result.type = "IP";
        result.ifname = FAKE_IFNAME;
        result.addresses = FAKE_ADDRESS;
        result.dnses = FAKE_DNS;
        result.gateways = FAKE_GATEWAY;
        result.pcscf = FAKE_PCSCF_ADDRESS;
        result.mtu = 1440;
        mSimulatedCommands.setDataCallResult(true, result);

        // Try to connect ENTERPRISE with the same CID as default
        replaceInstance(ConnectionParams.class, "mApnContext", mCp, mEnterpriseApnContext);
        doReturn(mApn1).when(mEnterpriseApnContext).getApnSetting();
        doReturn(ApnSetting.TYPE_ENTERPRISE_STRING).when(mEnterpriseApnContext).getApnType();
        doReturn(ApnSetting.TYPE_ENTERPRISE).when(mEnterpriseApnContext).getApnTypeBitmask();

        // Verify that ENTERPRISE wasn't set up
        connectEvent(false);
        assertEquals("DcInactiveState", getCurrentState().getName());

        // Change the CID
        result.cid = DEFAULT_DC_CID + 1;
        mSimulatedCommands.setDataCallResult(true, result);

        // Verify that ENTERPRISE was set up
        connectEvent(true);
        assertTrue(mDc.getNetworkCapabilities().hasCapability(
                NetworkCapabilities.NET_CAPABILITY_ENTERPRISE));
    }

    @Test
    public void testConnectEventNoDefaultData() throws Exception {
        assertFalse(mDefaultDc.isActive());

        // Try to connect ENTERPRISE when default data doesn't exist
        replaceInstance(ConnectionParams.class, "mApnContext", mCp, mEnterpriseApnContext);
        doReturn(mApn1).when(mEnterpriseApnContext).getApnSetting();
        doReturn(ApnSetting.TYPE_ENTERPRISE_STRING).when(mEnterpriseApnContext).getApnType();
        doReturn(ApnSetting.TYPE_ENTERPRISE).when(mEnterpriseApnContext).getApnTypeBitmask();

        // Verify that ENTERPRISE wasn't set up
        connectEvent(false);
        assertEquals("DcInactiveState", getCurrentState().getName());

        // Set up default data
        replaceInstance(ConnectionParams.class, "mApnContext", mCp, mApnContext);
        setUpDefaultData();

        // Verify that ENTERPRISE was set up
        replaceInstance(ConnectionParams.class, "mApnContext", mCp, mEnterpriseApnContext);
        connectEvent(true);
        assertTrue(mDc.getNetworkCapabilities().hasCapability(
                NetworkCapabilities.NET_CAPABILITY_ENTERPRISE));
    }

    private void setUpDefaultData() throws Exception {
        replaceInstance(DataConnection.class, "mCid", mDefaultDc, DEFAULT_DC_CID);
        doReturn(true).when(mDefaultDc).isActive();
        doReturn(Arrays.asList(mApnContext)).when(mDefaultDc).getApnContexts();
        mDcc.addActiveDcByCid(mDefaultDc);
        assertTrue(mDefaultDc.getApnContexts().stream()
                .anyMatch(apn -> apn.getApnTypeBitmask() == ApnSetting.TYPE_DEFAULT));
    }

    @Test
    @SmallTest
    public void testDisconnectEvent() throws Exception {
        testConnectEvent();

        mDc.setPduSessionId(5);
        mDc.sendMessage(DataConnection.EVENT_DISCONNECT, mDcp);
        waitForMs(100);
        disconnectEvent();

        verify(mSimulatedCommandsVerifier, times(1)).unregisterForLceInfo(any(Handler.class));
        verify(mSimulatedCommandsVerifier, times(1))
@@ -618,12 +700,9 @@ public class DataConnectionTest extends TelephonyTest {
                CarrierConfigManager.KEY_CARRIER_WWAN_DISALLOWED_APN_TYPES_STRING_ARRAY,
                new String[] {"supl"});

        mDc.sendMessage(DataConnection.EVENT_DISCONNECT, mDcp);
        waitForMs(100);
        disconnectEvent();
        doReturn(mApn1).when(mApnContext).getApnSetting();
        doReturn(ApnSetting.TYPE_ENTERPRISE).when(mApnContext).getApnTypeBitmask();
        mDc.sendMessage(DataConnection.EVENT_CONNECT, mCp);
        waitForMs(200);
        connectEvent(true);

        assertFalse("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN));
@@ -631,6 +710,42 @@ public class DataConnectionTest extends TelephonyTest {
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET));
        assertFalse("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL));
        assertFalse("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_ENTERPRISE));
    }

    @Test
    @SmallTest
    public void testEnterpriseNetworkCapability() throws Exception {
        mContextFixture.getCarrierConfigBundle().putStringArray(
                CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS,
                new String[] { "default" });
        doReturn(mApn2).when(mApnContext).getApnSetting();
        testConnectEvent();

        assertTrue("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN));
        assertTrue("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET));
        assertFalse("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS));
        assertFalse("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_ENTERPRISE));

        disconnectEvent();
        setUpDefaultData();
        replaceInstance(ConnectionParams.class, "mApnContext", mCp, mEnterpriseApnContext);
        doReturn(mApn1).when(mEnterpriseApnContext).getApnSetting();
        doReturn(ApnSetting.TYPE_ENTERPRISE_STRING).when(mEnterpriseApnContext).getApnType();
        doReturn(ApnSetting.TYPE_ENTERPRISE).when(mEnterpriseApnContext).getApnTypeBitmask();
        connectEvent(true);

        assertFalse("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN));
        assertFalse("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET));
        assertFalse("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL));
        assertTrue("capabilities: " + getNetworkCapabilities(), getNetworkCapabilities()
                .hasCapability(NetworkCapabilities.NET_CAPABILITY_ENTERPRISE));
    }
@@ -740,16 +855,19 @@ public class DataConnectionTest extends TelephonyTest {
        method.setAccessible(true);

        doReturn(apn).when(mApnContext).getApnSetting();
        connectEvent();
        doReturn(apn.getApnTypeBitmask()).when(mApnContext).getApnTypeBitmask();
        connectEvent(true);
        logd(getNetworkCapabilities().toString());

        return (Boolean) method.invoke(mDc);
    }

    private void connectEvent() throws Exception {
    private void connectEvent(boolean validate) {
        mDc.sendMessage(DataConnection.EVENT_CONNECT, mCp);
        waitForMs(200);
        assertEquals("DcActiveState", getCurrentState().getName());
        if (validate) {
            assertTrue(mDc.isActive());
        }
    }

    private void disconnectEvent() throws Exception {
@@ -760,7 +878,7 @@ public class DataConnectionTest extends TelephonyTest {

    @Test
    @SmallTest
    public void testIsIpAddress() throws Exception {
    public void testIsIpAddress() {
        // IPv4
        assertTrue(DataConnection.isIpAddress("1.2.3.4"));
        assertTrue(DataConnection.isIpAddress("127.0.0.1"));
@@ -1066,6 +1184,24 @@ public class DataConnectionTest extends TelephonyTest {
        assertTrue(isUnmeteredUseOnly());
    }

    @Test
    public void testIsEnterpriseUse() throws Exception {
        assertFalse(isEnterpriseUse());
        assertFalse(mDc.getNetworkCapabilities().hasCapability(
                NetworkCapabilities.NET_CAPABILITY_ENTERPRISE));

        setUpDefaultData();
        replaceInstance(ConnectionParams.class, "mApnContext", mCp, mEnterpriseApnContext);
        doReturn(mApn1).when(mEnterpriseApnContext).getApnSetting();
        doReturn(ApnSetting.TYPE_ENTERPRISE_STRING).when(mEnterpriseApnContext).getApnType();
        doReturn(ApnSetting.TYPE_ENTERPRISE).when(mEnterpriseApnContext).getApnTypeBitmask();
        connectEvent(true);

        assertTrue(isEnterpriseUse());
        assertTrue(mDc.getNetworkCapabilities().hasCapability(
                NetworkCapabilities.NET_CAPABILITY_ENTERPRISE));
    }

    @Test
    @SmallTest
    public void testGetDisallowedApnTypes() throws Exception {
+54 −25

File changed.

Preview size limit exceeded, changes collapsed.