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

Commit fa2e4e26 authored by William Escande's avatar William Escande
Browse files

Battery Profile: rm impossible null handler

Use final & injected dependency
Update test to TestLooper & truth
Split test to actionable smaller test
Force test to use disconnecting state

Bug: 367133232
Fix: 367133232
Test: atest BluetoothInstrumentationTests:BatteryServiceTest
Test: atest BluetoothInstrumentationTests:BatteryStateMachineTest
Flag: Exempt refactor no-op
Change-Id: Ia0187a77934262303dbf34d2f63f3cc64dc1ecaa
parent bb69ca6c
Loading
Loading
Loading
Loading
+45 −90
Original line number Diff line number Diff line
@@ -16,10 +16,11 @@

package com.android.bluetooth.bas;

import static java.util.Objects.requireNonNull;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
@@ -31,30 +32,45 @@ import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/** A profile service that connects to the Battery service (BAS) of BLE devices */
public class BatteryService extends ProfileService {
    private static final String TAG = "BatteryService";
    private static final String TAG = BatteryService.class.getSimpleName();

    // Timeout for state machine thread join, to prevent potential ANR.
    private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1_000;

    private static BatteryService sBatteryService;
    private AdapterService mAdapterService;
    private DatabaseManager mDatabaseManager;
    private HandlerThread mStateMachinesThread;
    private Handler mHandler;

    private final AdapterService mAdapterService;
    private final DatabaseManager mDatabaseManager;
    private final HandlerThread mStateMachinesThread;
    private final Handler mHandler;

    @GuardedBy("mStateMachines")
    private final Map<BluetoothDevice, BatteryStateMachine> mStateMachines = new HashMap<>();

    public BatteryService(Context ctx) {
        super(ctx);
    public BatteryService(AdapterService adapterService) {
        this(adapterService, Looper.getMainLooper());
    }

    @VisibleForTesting
    BatteryService(AdapterService adapterService, Looper looper) {
        super(requireNonNull(adapterService));
        mAdapterService = adapterService;
        mDatabaseManager = requireNonNull(mAdapterService.getDatabase());
        mHandler = new Handler(requireNonNull(looper));

        mStateMachinesThread = new HandlerThread("BatteryService.StateMachines");
        mStateMachinesThread.start();
        setBatteryService(this);
    }

    public static boolean isEnabled() {
@@ -66,38 +82,9 @@ public class BatteryService extends ProfileService {
        return null;
    }

    @Override
    public void start() {
        Log.d(TAG, "start()");
        if (sBatteryService != null) {
            throw new IllegalStateException("start() called twice");
        }

        mAdapterService =
                Objects.requireNonNull(
                        AdapterService.getAdapterService(),
                        "AdapterService cannot be null when BatteryService starts");
        mDatabaseManager =
                Objects.requireNonNull(
                        mAdapterService.getDatabase(),
                        "DatabaseManager cannot be null when BatteryService starts");

        mHandler = new Handler(Looper.getMainLooper());
        mStateMachines.clear();
        mStateMachinesThread = new HandlerThread("BatteryService.StateMachines");
        mStateMachinesThread.start();

        setBatteryService(this);
    }

    @Override
    public void stop() {
        Log.d(TAG, "stop()");
        if (sBatteryService == null) {
            Log.w(TAG, "stop() called before start()");
            return;
        }

        setBatteryService(null);

        // Destroy state machines and stop handler thread
@@ -109,23 +96,14 @@ public class BatteryService extends ProfileService {
            mStateMachines.clear();
        }

        if (mStateMachinesThread != null) {
        try {
            mStateMachinesThread.quitSafely();
            mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS);
                mStateMachinesThread = null;
        } catch (InterruptedException e) {
            // Do not rethrow as we are shutting down anyway
        }
        }

        // Unregister Handler and stop all queued messages.
        if (mHandler != null) {
        mHandler.removeCallbacksAndMessages(null);
            mHandler = null;
        }

        mAdapterService = null;
    }

    @Override
@@ -178,7 +156,7 @@ public class BatteryService extends ProfileService {
                Log.e(TAG, "Cannot connect to " + device + " : no state machine");
                return false;
            }
            sm.sendMessage(BatteryStateMachine.CONNECT);
            sm.sendMessage(BatteryStateMachine.MESSAGE_CONNECT);
        }

        return true;
@@ -208,7 +186,7 @@ public class BatteryService extends ProfileService {
        synchronized (mStateMachines) {
            BatteryStateMachine sm = getOrCreateStateMachine(device);
            if (sm != null) {
                sm.sendMessage(BatteryStateMachine.DISCONNECT);
                sm.sendMessage(BatteryStateMachine.MESSAGE_DISCONNECT);
            }
        }

@@ -231,12 +209,8 @@ public class BatteryService extends ProfileService {
    /**
     * Check whether it can connect to a peer device. The check considers a number of factors during
     * the evaluation.
     *
     * @param device the peer device to connect to
     * @return true if connection is allowed, otherwise false
     */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public boolean canConnect(BluetoothDevice device) {
    boolean canConnect(BluetoothDevice device) {
        // Check connectionPolicy and accept or reject the connection.
        int connectionPolicy = getConnectionPolicy(device);
        int bondState = mAdapterService.getBondState(device);
@@ -255,10 +229,8 @@ public class BatteryService extends ProfileService {
    }

    /** Called when the connection state of a state machine is changed */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public void handleConnectionStateChanged(BatteryStateMachine sm, int fromState, int toState) {
        BluetoothDevice device = sm.getDevice();
        if ((sm == null) || (fromState == toState)) {
    void handleConnectionStateChanged(BluetoothDevice device, int fromState, int toState) {
        if (fromState == toState) {
            Log.e(
                    TAG,
                    "connectionStateChanged: unexpected invocation. device="
@@ -366,16 +338,11 @@ public class BatteryService extends ProfileService {
    }

    /** Called when the battery level of the device is notified. */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public void handleBatteryChanged(BluetoothDevice device, int batteryLevel) {
    void handleBatteryChanged(BluetoothDevice device, int batteryLevel) {
        mAdapterService.setBatteryLevel(device, batteryLevel, /* isBas= */ true);
    }

    private BatteryStateMachine getOrCreateStateMachine(BluetoothDevice device) {
        if (device == null) {
            Log.e(TAG, "getOrCreateGatt failed: device cannot be null");
            return null;
        }
        synchronized (mStateMachines) {
            BatteryStateMachine sm = mStateMachines.get(device);
            if (sm != null) {
@@ -383,7 +350,7 @@ public class BatteryService extends ProfileService {
            }

            Log.d(TAG, "Creating a new state machine for " + device);
            sm = BatteryStateMachine.make(device, this, mStateMachinesThread.getLooper());
            sm = new BatteryStateMachine(this, device, mStateMachinesThread.getLooper());
            mStateMachines.put(device, sm);
            return sm;
        }
@@ -394,16 +361,8 @@ public class BatteryService extends ProfileService {
        mHandler.post(() -> bondStateChanged(device, toState));
    }

    /**
     * Remove state machine if the bonding for a device is removed
     *
     * @param device the device whose bonding state has changed
     * @param bondState the new bond state for the device. Possible values are: {@link
     *     BluetoothDevice#BOND_NONE}, {@link BluetoothDevice#BOND_BONDING}, {@link
     *     BluetoothDevice#BOND_BONDED}, {@link BluetoothDevice#ERROR}.
     */
    @VisibleForTesting
    void bondStateChanged(BluetoothDevice device, int bondState) {
    /** Remove state machine if the bonding for a device is removed */
    private void bondStateChanged(BluetoothDevice device, int bondState) {
        Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
        // Remove state machine if the bonding for a device is removed
        if (bondState != BluetoothDevice.BOND_NONE) {
@@ -423,16 +382,10 @@ public class BatteryService extends ProfileService {
    }

    private void removeStateMachine(BluetoothDevice device) {
        if (device == null) {
            Log.e(TAG, "removeStateMachine failed: device cannot be null");
            return;
        }
        synchronized (mStateMachines) {
            BatteryStateMachine sm = mStateMachines.remove(device);
            if (sm == null) {
                Log.w(
                        TAG,
                        "removeStateMachine: device " + device + " does not have a state machine");
                Log.w(TAG, "removeStateMachine: " + device + " does not have a state machine");
                return;
            }
            Log.i(TAG, "removeGatt: removing bluetooth gatt for device: " + device);
@@ -444,8 +397,10 @@ public class BatteryService extends ProfileService {
    @Override
    public void dump(StringBuilder sb) {
        super.dump(sb);
        synchronized (mStateMachines) {
            for (BatteryStateMachine sm : mStateMachines.values()) {
                sm.dump(sb);
            }
        }
    }
}
+120 −217

File changed.

Preview size limit exceeded, changes collapsed.

+90 −187
Original line number Diff line number Diff line
@@ -16,27 +16,33 @@

package com.android.bluetooth.bas;

import static android.bluetooth.BluetoothDevice.BOND_BONDED;
import static android.bluetooth.BluetoothDevice.BOND_BONDING;
import static android.bluetooth.BluetoothDevice.BOND_NONE;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.Looper;
import android.os.ParcelUuid;
import android.os.test.TestLooper;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;

import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.storage.DatabaseManager;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -46,239 +52,136 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.util.concurrent.TimeoutException;
import java.util.List;

@LargeTest
@MediumTest
@RunWith(JUnit4.class)
public class BatteryServiceTest {
    private BluetoothAdapter mAdapter;
    private Context mTargetContext;
    private BatteryService mService;
    private BluetoothDevice mDevice;
    private static final int CONNECTION_TIMEOUT_MS = 1000;
    @Rule public final MockitoRule mockito = MockitoJUnit.rule();

    @Mock private AdapterService mAdapterService;
    @Mock private DatabaseManager mDatabaseManager;

    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
    private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
    private final BluetoothDevice mDevice = TestUtils.getTestDevice(mAdapter, 78);

    private BatteryService mService;
    private TestLooper mLooper;

    @Before
    public void setUp() throws Exception {
        mTargetContext = InstrumentationRegistry.getTargetContext();
        if (Looper.myLooper() == null) {
            Looper.prepare();
        }
    public void setUp() {
        mLooper = new TestLooper();

        TestUtils.setAdapterService(mAdapterService);
        doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
        doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService).getBondState(any());

        mAdapter = BluetoothAdapter.getDefaultAdapter();

        startService();

        // Override the timeout value to speed up the test
        BatteryStateMachine.sConnectTimeoutMs = CONNECTION_TIMEOUT_MS; // 1s

        // Get a device for testing
        mDevice = TestUtils.getTestDevice(mAdapter, 0);
        doReturn(BluetoothDevice.BOND_BONDED)
                .when(mAdapterService)
                .getBondState(any(BluetoothDevice.class));
        mService = new BatteryService(mAdapterService, mLooper.getLooper());
        mService.setAvailable(true);
    }

    @After
    public void tearDown() throws Exception {
        stopService();
        TestUtils.clearAdapterService(mAdapterService);
    public void tearDown() {
        // To prevent double stop
        if (BatteryService.getBatteryService() != null) {
            mService.stop();
        }

    private void startService() throws TimeoutException {
        mService = new BatteryService(mTargetContext);
        mService.start();
        mService.setAvailable(true);
        assertThat(BatteryService.getBatteryService()).isNull();
    }

    private void stopService() throws TimeoutException {
        mService.stop();
        mService = BatteryService.getBatteryService();
        Assert.assertNull(mService);
    @Test
    public void getBatteryService() {
        assertThat(BatteryService.getBatteryService()).isEqualTo(mService);
    }

    /** Test get Battery Service */
    @Test
    public void testGetBatteryService() {
        Assert.assertEquals(mService, BatteryService.getBatteryService());
    public void setConnectionPolicy() {
        assertThat(mService.setConnectionPolicy(mDevice, CONNECTION_POLICY_FORBIDDEN)).isTrue();
    }

    /** Test get/set policy for BluetoothDevice */
    @Test
    public void testGetSetPolicy() {
        when(mDatabaseManager.getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
                .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
        Assert.assertEquals(
                "Initial device policy",
                BluetoothProfile.CONNECTION_POLICY_UNKNOWN,
                mService.getConnectionPolicy(mDevice));

        when(mDatabaseManager.getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
                .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
        Assert.assertEquals(
                "Setting device policy to POLICY_FORBIDDEN",
                BluetoothProfile.CONNECTION_POLICY_FORBIDDEN,
                mService.getConnectionPolicy(mDevice));

        when(mDatabaseManager.getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
                .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
        Assert.assertEquals(
                "Setting device policy to POLICY_ALLOWED",
                BluetoothProfile.CONNECTION_POLICY_ALLOWED,
                mService.getConnectionPolicy(mDevice));
    public void getConnectionPolicy() {
        for (int policy :
                List.of(
                        CONNECTION_POLICY_UNKNOWN,
                        CONNECTION_POLICY_FORBIDDEN,
                        CONNECTION_POLICY_ALLOWED)) {
            doReturn(policy).when(mDatabaseManager).getProfileConnectionPolicy(any(), anyInt());
            assertThat(mService.getConnectionPolicy(mDevice)).isEqualTo(policy);
        }
    }

    /** Test if getProfileConnectionPolicy works after the service is stopped. */
    @Test
    public void testGetPolicyAfterStopped() {
        mService.stop();
        when(mDatabaseManager.getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
                .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
        Assert.assertEquals(
                "Initial device policy",
                BluetoothProfile.CONNECTION_POLICY_UNKNOWN,
                mService.getConnectionPolicy(mDevice));
    public void canConnect_whenNotBonded_returnFalse() {
        int badPolicyValue = 1024;
        int badBondState = 42;
        for (int bondState : List.of(BOND_NONE, BOND_BONDING, badBondState)) {
            for (int policy :
                    List.of(
                            CONNECTION_POLICY_UNKNOWN,
                            CONNECTION_POLICY_FORBIDDEN,
                            CONNECTION_POLICY_ALLOWED,
                            badPolicyValue)) {
                doReturn(bondState).when(mAdapterService).getBondState(any());
                doReturn(policy).when(mDatabaseManager).getProfileConnectionPolicy(any(), anyInt());
                assertThat(mService.canConnect(mDevice)).isEqualTo(false);
            }
        }
    }

    /** Test okToConnect method using various test cases */
    @Test
    public void testCanConnect() {
    public void canConnect_whenBonded() {
        int badPolicyValue = 1024;
        int badBondState = 42;
        testCanConnectCase(
                mDevice,
                BluetoothDevice.BOND_NONE,
                BluetoothProfile.CONNECTION_POLICY_UNKNOWN,
                false);
        testCanConnectCase(
                mDevice,
                BluetoothDevice.BOND_NONE,
                BluetoothProfile.CONNECTION_POLICY_FORBIDDEN,
                false);
        testCanConnectCase(
                mDevice,
                BluetoothDevice.BOND_NONE,
                BluetoothProfile.CONNECTION_POLICY_ALLOWED,
                false);
        testCanConnectCase(mDevice, BluetoothDevice.BOND_NONE, badPolicyValue, false);
        testCanConnectCase(
                mDevice,
                BluetoothDevice.BOND_BONDING,
                BluetoothProfile.CONNECTION_POLICY_UNKNOWN,
                false);
        testCanConnectCase(
                mDevice,
                BluetoothDevice.BOND_BONDING,
                BluetoothProfile.CONNECTION_POLICY_FORBIDDEN,
                false);
        testCanConnectCase(
                mDevice,
                BluetoothDevice.BOND_BONDING,
                BluetoothProfile.CONNECTION_POLICY_ALLOWED,
                false);
        testCanConnectCase(mDevice, BluetoothDevice.BOND_BONDING, badPolicyValue, false);
        testCanConnectCase(
                mDevice,
                BluetoothDevice.BOND_BONDED,
                BluetoothProfile.CONNECTION_POLICY_UNKNOWN,
                true);
        testCanConnectCase(
                mDevice,
                BluetoothDevice.BOND_BONDED,
                BluetoothProfile.CONNECTION_POLICY_FORBIDDEN,
                false);
        testCanConnectCase(
                mDevice,
                BluetoothDevice.BOND_BONDED,
                BluetoothProfile.CONNECTION_POLICY_ALLOWED,
                true);
        testCanConnectCase(mDevice, BluetoothDevice.BOND_BONDED, badPolicyValue, false);
        testCanConnectCase(
                mDevice, badBondState, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, false);
        testCanConnectCase(
                mDevice, badBondState, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false);
        testCanConnectCase(
                mDevice, badBondState, BluetoothProfile.CONNECTION_POLICY_ALLOWED, false);
        testCanConnectCase(mDevice, badBondState, badPolicyValue, false);
        doReturn(BOND_BONDED).when(mAdapterService).getBondState(any());

        for (int policy : List.of(CONNECTION_POLICY_FORBIDDEN, badPolicyValue)) {
            doReturn(policy).when(mDatabaseManager).getProfileConnectionPolicy(any(), anyInt());
            assertThat(mService.canConnect(mDevice)).isEqualTo(false);
        }
        for (int policy : List.of(CONNECTION_POLICY_UNKNOWN, CONNECTION_POLICY_ALLOWED)) {
            doReturn(policy).when(mDatabaseManager).getProfileConnectionPolicy(any(), anyInt());
            assertThat(mService.canConnect(mDevice)).isEqualTo(true);
        }
    }

    /** Test that an outgoing connection to device */
    @Test
    public void testConnectAndDump() {
        // Update the device policy so okToConnect() returns true
        when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
        when(mDatabaseManager.getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
                .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
        // Return Battery UUID
    public void connectAndDump_doesNotCrash() {
        doReturn(CONNECTION_POLICY_ALLOWED)
                .when(mDatabaseManager)
                .getProfileConnectionPolicy(any(), anyInt());

        doReturn(new ParcelUuid[] {BluetoothUuid.BATTERY})
                .when(mAdapterService)
                .getRemoteUuids(any(BluetoothDevice.class));
        // Send a connect request
        Assert.assertTrue("Connect expected to succeed", mService.connect(mDevice));

        // Test dump() is not crashed.
        assertThat(mService.connect(mDevice)).isTrue();

        mService.dump(new StringBuilder());
    }

    /** Test that an outgoing connection to device with POLICY_FORBIDDEN is rejected */
    @Test
    public void testForbiddenPolicy_FailsToConnect() {
        // Set the device policy to POLICY_FORBIDDEN so connect() should fail
        when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
        when(mDatabaseManager.getProfileConnectionPolicy(mDevice, BluetoothProfile.BATTERY))
                .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);

        // Send a connect request
        Assert.assertFalse("Connect expected to fail", mService.connect(mDevice));
    public void connect_whenForbiddenPolicy_FailsToConnect() {
        doReturn(CONNECTION_POLICY_FORBIDDEN)
                .when(mDatabaseManager)
                .getProfileConnectionPolicy(any(), anyInt());

        assertThat(mService.connect(mDevice)).isFalse();
    }

    @Test
    public void getConnectionState_whenNoDevicesAreConnected_returnsDisconnectedState() {
        Assert.assertEquals(
                mService.getConnectionState(mDevice), BluetoothProfile.STATE_DISCONNECTED);
        assertThat(mService.getConnectionState(mDevice)).isEqualTo(STATE_DISCONNECTED);
    }

    @Test
    public void getDevices_whenNoDevicesAreConnected_returnsEmptyList() {
        Assert.assertTrue(mService.getDevices().isEmpty());
        assertThat(mService.getDevices()).isEmpty();
    }

    @Test
    public void getDevicesMatchingConnectionStates() {
        when(mAdapterService.getBondedDevices()).thenReturn(new BluetoothDevice[] {mDevice});
        int states[] = new int[] {BluetoothProfile.STATE_DISCONNECTED};

        Assert.assertTrue(mService.getDevicesMatchingConnectionStates(states).contains(mDevice));
    }

    @Test
    public void setConnectionPolicy() {
        Assert.assertTrue(
                mService.setConnectionPolicy(
                        mDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN));
    }
        doReturn(new BluetoothDevice[] {mDevice}).when(mAdapterService).getBondedDevices();
        int[] states = new int[] {STATE_DISCONNECTED};

    /**
     * Helper function to test okToConnect() method
     *
     * @param device test device
     * @param bondState bond state value, could be invalid
     * @param policy value, could be invalid
     * @param expected expected result from okToConnect()
     */
    private void testCanConnectCase(
            BluetoothDevice device, int bondState, int policy, boolean expected) {
        doReturn(bondState).when(mAdapterService).getBondState(device);
        when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
        when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.BATTERY))
                .thenReturn(policy);
        Assert.assertEquals(expected, mService.canConnect(device));
        assertThat(mService.getDevicesMatchingConnectionStates(states)).containsExactly(mDevice);
    }
}
+147 −229

File changed.

Preview size limit exceeded, changes collapsed.