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

Commit 4de0c3ea authored by Yiyi Shen's avatar Yiyi Shen Committed by Android (Google) Code Review
Browse files

Merge "[Audiosharing] Log start broadcast and add source with sharing state" into main

parents ef1f0f19 3645e2d7
Loading
Loading
Loading
Loading
+45 −21
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.settings.connecteddevice.audiosharing;

import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_IS_PRIMARY;
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_IS_TEMP_BOND;
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING;

import android.app.settings.SettingsEnums;
@@ -63,6 +65,7 @@ public class AudioSharingDialogHandler {
    private static final String TAG = "AudioSharingDlgHandler";
    private final Context mContext;
    private final Fragment mHostFragment;
    private final int mHostMetricsCategory;
    @Nullable private final LocalBluetoothManager mLocalBtManager;
    @Nullable private final CachedBluetoothDeviceManager mDeviceManager;
    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
@@ -121,7 +124,7 @@ public class AudioSharingDialogHandler {
                        mMetricsFeatureProvider.action(
                                mContext,
                                SettingsEnums.ACTION_AUDIO_SHARING_STOP_FAILED,
                                SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY);
                                mHostMetricsCategory);
                        AudioSharingUtils.toastMessage(
                                mContext, "Fail to stop broadcast, reason " + reason);
                        mIsStoppingBroadcast = false;
@@ -163,6 +166,9 @@ public class AudioSharingDialogHandler {
                        : null;
        mAudioManager = context.getSystemService(AudioManager.class);
        mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
        mHostMetricsCategory = mHostFragment instanceof DashboardFragment
                ? ((DashboardFragment) mHostFragment).getMetricsCategory()
                : SettingsEnums.PAGE_UNKNOWN;
    }

    /** Register callbacks for dialog handler */
@@ -249,7 +255,7 @@ public class AudioSharingDialogHandler {
                    };
            Pair<Integer, Object>[] eventData =
                    AudioSharingUtils.buildAudioSharingDialogEventData(
                            SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
                            mHostMetricsCategory,
                            SettingsEnums.DIALOG_STOP_AUDIO_SHARING,
                            userTriggered,
                            deviceItemsInSharingSession.size(),
@@ -294,6 +300,10 @@ public class AudioSharingDialogHandler {
                                                    device, mLocalBtManager))) {
                Log.d(TAG, "Auto add sink with the same group to the sharing: " + deviceAddress);
                if (mAssistant != null && mBroadcast != null) {
                    mMetricsFeatureProvider.action(mContext,
                            SettingsEnums.ACTION_AUDIO_SHARING_ADD_SOURCE,
                            AudioSharingUtils.buildAddSourceEventData(
                                    mHostMetricsCategory, /* userTriggered= */false));
                    mAssistant.addSource(
                            btDevice,
                            mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
@@ -315,11 +325,14 @@ public class AudioSharingDialogHandler {
                            // Remove all sources from the device user clicked
                            removeSourceForGroup(item.getGroupId(), groupedDevices);
                            // Add current broadcast to the latest connected device
                            addSourceForGroup(groupId, groupedDevices);
                            addSourceForGroup(groupId, groupedDevices,
                                    AudioSharingUtils.buildAddSourceEventData(
                                            SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE,
                                            userTriggered));
                        };
                Pair<Integer, Object>[] eventData =
                        AudioSharingUtils.buildAudioSharingDialogEventData(
                                SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
                                mHostMetricsCategory,
                                SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE,
                                userTriggered,
                                deviceItemsInSharingSession.size(),
@@ -340,7 +353,10 @@ public class AudioSharingDialogHandler {
                        new AudioSharingJoinDialogFragment.DialogEventListener() {
                            @Override
                            public void onShareClick() {
                                addSourceForGroup(groupId, groupedDevices);
                                addSourceForGroup(groupId, groupedDevices,
                                        AudioSharingUtils.buildAddSourceEventData(
                                                SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
                                                userTriggered));
                            }

                            @Override
@@ -348,7 +364,7 @@ public class AudioSharingDialogHandler {
                        };
                Pair<Integer, Object>[] eventData =
                        AudioSharingUtils.buildAudioSharingDialogEventData(
                                SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
                                mHostMetricsCategory,
                                SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
                                userTriggered,
                                deviceItemsInSharingSession.size(),
@@ -389,11 +405,7 @@ public class AudioSharingDialogHandler {
                                new SubSettingLauncher(mContext)
                                        .setDestination(
                                                AudioSharingDashboardFragment.class.getName())
                                        .setSourceMetricsCategory(
                                                (mHostFragment instanceof DashboardFragment)
                                                        ? ((DashboardFragment) mHostFragment)
                                                                .getMetricsCategory()
                                                        : SettingsEnums.PAGE_UNKNOWN)
                                        .setSourceMetricsCategory(mHostMetricsCategory)
                                        .setArguments(args)
                                        .launch();
                            }
@@ -408,7 +420,7 @@ public class AudioSharingDialogHandler {

                Pair<Integer, Object>[] eventData =
                        AudioSharingUtils.buildAudioSharingDialogEventData(
                                SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
                                mHostMetricsCategory,
                                SettingsEnums.DIALOG_START_AUDIO_SHARING,
                                userTriggered,
                                /* deviceCountInSharing= */ 0,
@@ -533,18 +545,28 @@ public class AudioSharingDialogHandler {
            Log.d(TAG, "Fail to remove source for group " + groupId);
            return;
        }
        groupedDevices.getOrDefault(groupId, ImmutableList.of()).stream()
                .forEach(
        List<BluetoothDevice> devices = groupedDevices.get(groupId);
        devices.stream().forEach(
                device -> {
                    for (BluetoothLeBroadcastReceiveState source :
                            mAssistant.getAllSources(device)) {
                        mAssistant.removeSource(device, source.getSourceId());
                    }
                });
        boolean isPrimary = groupId == BluetoothUtils.getPrimaryGroupIdForBroadcast(
                mContext.getContentResolver(), mLocalBtManager);
        boolean isTempBond = devices.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice);
        Pair<Integer, Object>[] eventData = new Pair[]{
                Pair.create(METRIC_KEY_DEVICE_IS_PRIMARY.getId(), isPrimary ? 1 : 0),
                Pair.create(METRIC_KEY_DEVICE_IS_TEMP_BOND.getId(), isTempBond ? 1 : 0)
        };
        mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUDIO_SHARING_REMOVE_SOURCE,
                eventData);
    }

    private void addSourceForGroup(
            int groupId, Map<Integer, List<BluetoothDevice>> groupedDevices) {
            int groupId, Map<Integer, List<BluetoothDevice>> groupedDevices,
            Pair<Integer, Object>[] eventData) {
        if (mBroadcast == null || mAssistant == null) {
            Log.d(TAG, "Fail to add source due to null profiles, group = " + groupId);
            return;
@@ -560,6 +582,8 @@ public class AudioSharingDialogHandler {
                                        device,
                                        mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
                                        /* isGroupOp= */ false));
        mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUDIO_SHARING_ADD_SOURCE,
                eventData);
    }

    private boolean isBroadcasting() {
+14 −2
Original line number Diff line number Diff line
@@ -100,11 +100,10 @@ public class AudioSharingReceiver extends BroadcastReceiver {
                    //       isLeAudioBroadcastAssistantSupported() always return FEATURE_SUPPORTED
                    //       or FEATURE_NOT_SUPPORTED when BT and BLE off
                    cancelSharingNotification(context, AUDIO_SHARING_NOTIFICATION_ID);
                    cancelSharingNotification(context, ADD_SOURCE_NOTIFICATION_ID);
                    metricsFeatureProvider.action(
                            context, SettingsEnums.ACTION_CANCEL_AUDIO_SHARING_NOTIFICATION,
                            LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_STATE_CHANGE);
                    cancelSharingNotification(context, ADD_SOURCE_NOTIFICATION_ID);
                    // TODO: add metric
                } else {
                    Log.w(
                            TAG,
@@ -156,6 +155,8 @@ public class AudioSharingReceiver extends BroadcastReceiver {
                    LocalBluetoothManager manager = Utils.getLocalBtManager(context);
                    if (!validToAddSource(device, action, manager).isEmpty()) {
                        showAddSourceNotification(context, device);
                        metricsFeatureProvider.action(
                                context, SettingsEnums.ACTION_SHOW_ADD_SOURCE_NOTIFICATION);
                    }
                }
                break;
@@ -172,11 +173,22 @@ public class AudioSharingReceiver extends BroadcastReceiver {
                ImmutableList<BluetoothDevice> sinksToAdd = validToAddSource(sink, action, manager);
                AudioSharingUtils.addSourceToTargetSinks(sinksToAdd, manager);
                cancelSharingNotification(context, ADD_SOURCE_NOTIFICATION_ID);
                if (!sinksToAdd.isEmpty()) {
                    metricsFeatureProvider.action(context,
                            SettingsEnums.ACTION_AUDIO_SHARING_ADD_SOURCE,
                            AudioSharingUtils.buildAddSourceEventData(
                                    SettingsEnums.ACTION_SHOW_ADD_SOURCE_NOTIFICATION,
                                    /* userTriggered= */ false));
                }
                break;
            case ACTION_LE_AUDIO_SHARING_CANCEL_NOTIF:
                int notifId = intent.getIntExtra(EXTRA_NOTIF_ID, -1);
                if (notifId != -1) {
                    cancelSharingNotification(context, notifId);
                    if (notifId == ADD_SOURCE_NOTIFICATION_ID) {
                        metricsFeatureProvider.action(
                                context, SettingsEnums.ACTION_CANCEL_ADD_SOURCE_NOTIFICATION);
                    }
                }
                break;
            default:
+86 −29
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;

@@ -85,6 +86,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
        BluetoothCallback {
    private static final String TAG = "AudioSharingSwitchCtlr";
    private static final String PREF_KEY = "audio_sharing_main_switch";
    private static final String EXTRA_SOURCE_METRICS = ":settings:source_metrics";

    interface OnAudioSharingStateChangedListener {
        /**
@@ -120,7 +122,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
    private List<AudioSharingDeviceItem> mDeviceItemsForSharing = new ArrayList<>();
    private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
    private AtomicInteger mIntentHandleStage =
            new AtomicInteger(StartIntentHandleStage.TO_HANDLE.ordinal());
            new AtomicInteger(StartIntentHandleStage.TO_HANDLE.getId());
    // The sinks in adding source process. We show the progress dialog based on this list.
    private CopyOnWriteArrayList<BluetoothDevice> mSinksInAdding = new CopyOnWriteArrayList<>();
    // The primary/active sinks in adding source process.
@@ -280,7 +282,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
                            Pair<Integer, Object>[] eventData =
                                    AudioSharingUtils.buildAudioSharingDialogEventData(
                                            SettingsEnums.AUDIO_SHARING_SETTINGS,
                                            SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
                                            SettingsEnums.DIALOG_AUDIO_SHARING_MAIN,
                                            /* userTriggered= */ false,
                                            /* deviceCountInSharing= */ 1,
                                            /* candidateDeviceCount= */ 0);
@@ -379,8 +381,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
            return;
        }
        if (mIntentHandleStage.compareAndSet(
                StartIntentHandleStage.TO_HANDLE.ordinal(),
                StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal())) {
                StartIntentHandleStage.TO_HANDLE.getId(),
                StartIntentHandleStage.HANDLE_AUTO_ADD.getId())) {
            Log.d(TAG, "onStart: handleStartAudioSharingFromIntent");
            handleStartAudioSharingFromIntent();
        }
@@ -462,8 +464,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
                mProfileManager.removeServiceListener(this);
            }
            if (mIntentHandleStage.compareAndSet(
                    StartIntentHandleStage.TO_HANDLE.ordinal(),
                    StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal())) {
                    StartIntentHandleStage.TO_HANDLE.getId(),
                    StartIntentHandleStage.HANDLE_AUTO_ADD.getId())) {
                Log.d(TAG, "onServiceConnected: handleStartAudioSharingFromIntent");
                handleStartAudioSharingFromIntent();
            }
@@ -505,7 +507,9 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
        if (cachedDevice != null && mBroadcast != null) {
            Log.d(TAG, "handleAutoAddSourceAfterPair, device = " + device.getAnonymizedAddress());
            addSourceToTargetSinks(ImmutableList.of(device), cachedDevice.getName(),
                    mBroadcast.getLatestBluetoothLeBroadcastMetadata());
                    mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
                    AudioSharingUtils.buildAddSourceEventData(
                            SettingsEnums.BLUETOOTH_PAIRING, /* userTriggered= */true));
        }
    }

@@ -577,10 +581,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
                    () -> AudioSharingProgressDialogFragment.show(mFragment,
                            mContext.getString(
                                    R.string.audio_sharing_progress_dialog_start_stream_content)));
            mMetricsFeatureProvider.action(
                    mContext,
                    SettingsEnums.ACTION_AUDIO_SHARING_MAIN_SWITCH_ON,
                    deviceItems.size());
            logStartBroadcast(deviceItems.size());
        }
    }

@@ -651,14 +652,17 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
        Pair<Integer, Object>[] eventData =
                AudioSharingUtils.buildAudioSharingDialogEventData(
                        SettingsEnums.AUDIO_SHARING_SETTINGS,
                        SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
                        SettingsEnums.DIALOG_AUDIO_SHARING_MAIN,
                        /* userTriggered= */ false,
                        /* deviceCountInSharing= */ targetActiveSinks.isEmpty() ? 0 : 1,
                        /* candidateDeviceCount= */ mDeviceItemsForSharing.size());
        // Auto add primary/active sinks w/o user interactions.
        if (!targetActiveSinks.isEmpty() && mTargetActiveItem != null) {
            Log.d(TAG, "handleOnBroadcastReady: automatically add source to active sinks.");
            addSourceToTargetSinks(targetActiveSinks, mTargetActiveItem.getName(), metadata);
            addSourceToTargetSinks(targetActiveSinks, mTargetActiveItem.getName(), metadata,
                    AudioSharingUtils.buildAddSourceEventData(
                            SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING,
                            /* userTriggered= */false));
            // To avoid users advance to share then pair flow before the primary/active sinks
            // successfully join the audio sharing, save the primary/active sinks in mSinksToWaitFor
            // and popup dialog till adding source complete for these sinks.
@@ -666,29 +670,30 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
                mSinksToWaitFor.clear();
                mSinksToWaitFor.addAll(targetActiveSinks);
            }
            mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING);
            mTargetActiveItem = null;
            // When audio sharing page is brought up by intent with EXTRA_START_LE_AUDIO_SHARING
            // == true, plus there is one active lea headset and one connected lea headset, we
            // should auto add these sinks without user interactions.
            if (mIntentHandleStage.compareAndSet(
                    StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
                    StartIntentHandleStage.HANDLED.ordinal())
                    StartIntentHandleStage.HANDLE_AUTO_ADD.getId(),
                    StartIntentHandleStage.HANDLED.getId())
                    && mDeviceItemsForSharing.size() == 1) {
                Log.d(TAG, "handleOnBroadcastReady: auto add source to the second device");
                AudioSharingDeviceItem target = mDeviceItemsForSharing.get(0);
                List<BluetoothDevice> targetSinks = mGroupedConnectedDevices.getOrDefault(
                        target.getGroupId(), ImmutableList.of());
                addSourceToTargetSinks(targetSinks, target.getName(), metadata);
                addSourceToTargetSinks(targetSinks, target.getName(), metadata,
                        AudioSharingUtils.buildAddSourceEventData(
                                SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING,
                                /* userTriggered= */true));
                cleanUpStatesForStartSharing();
                // TODO: Add metric for auto add by intent
                return;
            }
        }
        // Still mark intent as handled if early returned due to preconditions not met
        mIntentHandleStage.compareAndSet(
                StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
                StartIntentHandleStage.HANDLED.ordinal());
                StartIntentHandleStage.HANDLE_AUTO_ADD.getId(),
                StartIntentHandleStage.HANDLED.getId());
        if (mFragment == null) {
            Log.d(TAG, "handleOnBroadcastReady: dialog fail to show due to null fragment.");
            // Clean up states before early return.
@@ -723,7 +728,10 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
                    public void onItemClick(@NonNull AudioSharingDeviceItem item) {
                        List<BluetoothDevice> targetSinks = mGroupedConnectedDevices.getOrDefault(
                                item.getGroupId(), ImmutableList.of());
                        addSourceToTargetSinks(targetSinks, item.getName(), metadata);
                        addSourceToTargetSinks(targetSinks, item.getName(), metadata,
                                AudioSharingUtils.buildAddSourceEventData(
                                        SettingsEnums.DIALOG_AUDIO_SHARING_MAIN,
                                        /* userTriggered= */true));
                        cleanUpStatesForStartSharing();
                    }

@@ -812,25 +820,27 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
                            if (!shouldStart) {
                                Log.d(TAG, "Skip handleStartAudioSharingFromIntent, arg false");
                                mIntentHandleStage.compareAndSet(
                                        StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
                                        StartIntentHandleStage.HANDLED.ordinal());
                                        StartIntentHandleStage.HANDLE_AUTO_ADD.getId(),
                                        StartIntentHandleStage.HANDLED.getId());
                                return;
                            }
                            if (BluetoothUtils.isBroadcasting(mBtManager)) {
                                Log.d(TAG, "Skip handleStartAudioSharingFromIntent, in broadcast");
                                mIntentHandleStage.compareAndSet(
                                        StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(),
                                        StartIntentHandleStage.HANDLED.ordinal());
                                        StartIntentHandleStage.HANDLE_AUTO_ADD.getId(),
                                        StartIntentHandleStage.HANDLED.getId());
                                return;
                            }
                            Log.d(TAG, "HandleStartAudioSharingFromIntent, start broadcast");
                            //
                            AudioSharingUtils.postOnMainThread(
                                    mContext, () -> mSwitchBar.setChecked(true));
                        });
    }

    private void addSourceToTargetSinks(List<BluetoothDevice> targetGroupedSinks,
            @NonNull String targetSinkName, @Nullable BluetoothLeBroadcastMetadata metadata) {
            @NonNull String targetSinkName, @Nullable BluetoothLeBroadcastMetadata metadata,
            Pair<Integer, Object>[] eventData) {
        if (targetGroupedSinks.isEmpty()) {
            Log.d(TAG, "Skip addSourceToTargetSinks, no sinks.");
            return;
@@ -850,6 +860,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
        for (BluetoothDevice sink : targetGroupedSinks) {
            mAssistant.addSource(sink, metadata, /* isGroupOp= */ false);
        }
        mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUDIO_SHARING_ADD_SOURCE,
                eventData);
    }

    private void showProgressDialog(@NonNull String progressMessage) {
@@ -869,9 +881,54 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
        mDeviceItemsForSharing.clear();
    }

    private void logStartBroadcast(int candidateDeviceCount) {
        int sourceMetric = SettingsEnums.PAGE_UNKNOWN;
        String callingPackage = "";
        if (mIntentHandleStage.get() != StartIntentHandleStage.HANDLE_AUTO_ADD.getId()) {
            // Sharing is started by toggle
            sourceMetric = SettingsEnums.AUDIO_SHARING_SETTINGS;
            callingPackage = mContext.getPackageName();
        } else if (mFragment != null && mFragment.getActivity() != null) {
            // Sharing is started by intent
            FragmentActivity activity = mFragment.getActivity();
            Intent intent = activity.getIntent();
            if (intent != null) {
                sourceMetric = intent.getIntExtra(EXTRA_SOURCE_METRICS,
                        SettingsEnums.PAGE_UNKNOWN);
            }
            if (activity instanceof SettingsActivity settingsActivity) {
                callingPackage = settingsActivity.getInitialCallingPackage();
            }
        }
        Pair<Integer, Object>[] eventData = new Pair[]{
                Pair.create(AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.getId(),
                        sourceMetric),
                Pair.create(
                        AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PACKAGE_NAME.getId(),
                        callingPackage),
                Pair.create(
                        AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT.getId(),
                        candidateDeviceCount)
        };
        mMetricsFeatureProvider.action(
                mContext,
                SettingsEnums.ACTION_AUDIO_SHARING_MAIN_SWITCH_ON,
                eventData);
    }

    private enum StartIntentHandleStage {
        TO_HANDLE,
        HANDLE_AUTO_ADD,
        HANDLED,
        TO_HANDLE(0),
        HANDLE_AUTO_ADD(1),
        HANDLED(2);

        private final int mId;

        StartIntentHandleStage(int id) {
            this.mId = id;
        }

        public int getId() {
            return mId;
        }
    }
}
+26 −6

File changed.

Preview size limit exceeded, changes collapsed.

+94 −49

File changed.

Preview size limit exceeded, changes collapsed.

Loading