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

Commit 0c0eefe2 authored by Charles Chen's avatar Charles Chen
Browse files

Add support to launch Overlay container

Bug: 243518738
Test: atest OverlayPresentationTest
Change-Id: Ie26f6d4e6f6330649bb11a34ec98022196aa2dd7
parent 54975542
Loading
Loading
Loading
Loading
+119 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.window.extensions.embedding;

import static java.util.Objects.requireNonNull;

import android.graphics.Rect;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

/**
 * The parameter to create an overlay container that retrieved from
 * {@link android.app.ActivityOptions} bundle.
 */
class OverlayCreateParams {

    // TODO(b/295803704): Move them to WM Extensions so that we can reuse in WM Jetpack.
    @VisibleForTesting
    static final String KEY_OVERLAY_CREATE_PARAMS =
            "androidx.window.extensions.OverlayCreateParams";

    @VisibleForTesting
    static final String KEY_OVERLAY_CREATE_PARAMS_TASK_ID =
            "androidx.window.extensions.OverlayCreateParams.taskId";

    @VisibleForTesting
    static final String KEY_OVERLAY_CREATE_PARAMS_TAG =
            "androidx.window.extensions.OverlayCreateParams.tag";

    @VisibleForTesting
    static final String KEY_OVERLAY_CREATE_PARAMS_BOUNDS =
            "androidx.window.extensions.OverlayCreateParams.bounds";

    private final int mTaskId;

    @NonNull
    private final String mTag;

    @NonNull
    private final Rect mBounds;

    OverlayCreateParams(int taskId, @NonNull String tag, @NonNull Rect bounds) {
        mTaskId = taskId;
        mTag = requireNonNull(tag);
        mBounds = requireNonNull(bounds);
    }

    int getTaskId() {
        return mTaskId;
    }

    @NonNull
    String getTag() {
        return mTag;
    }

    @NonNull
    Rect getBounds() {
        return mBounds;
    }

    @Override
    public int hashCode() {
        int result = mTaskId;
        result = 31 * result + mTag.hashCode();
        result = 31 * result + mBounds.hashCode();
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (!(obj instanceof OverlayCreateParams thatParams)) return false;
        return mTaskId == thatParams.mTaskId
                && mTag.equals(thatParams.mTag)
                && mBounds.equals(thatParams.mBounds);
    }

    @Override
    public String toString() {
        return OverlayCreateParams.class.getSimpleName() + ": {"
                + "taskId=" + mTaskId
                + ", tag=" + mTag
                + ", bounds=" + mBounds
                + "}";
    }

    /** Retrieves the {@link OverlayCreateParams} from {@link android.app.ActivityOptions} bundle */
    @Nullable
    static OverlayCreateParams fromBundle(@NonNull Bundle bundle) {
        final Bundle paramsBundle = bundle.getBundle(KEY_OVERLAY_CREATE_PARAMS);
        if (paramsBundle == null) {
            return null;
        }
        final int taskId = paramsBundle.getInt(KEY_OVERLAY_CREATE_PARAMS_TASK_ID);
        final String tag = requireNonNull(paramsBundle.getString(KEY_OVERLAY_CREATE_PARAMS_TAG));
        final Rect bounds = requireNonNull(paramsBundle.getParcelable(
                KEY_OVERLAY_CREATE_PARAMS_BOUNDS, Rect.class));

        return new OverlayCreateParams(taskId, tag, bounds);
    }
}
+181 −21
Original line number Diff line number Diff line
@@ -40,9 +40,10 @@ import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceh
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent;
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO;
import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions;
import static androidx.window.extensions.embedding.SplitPresenter.getActivitiesMinDimensionsPair;
import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair;
import static androidx.window.extensions.embedding.SplitPresenter.getTaskWindowMetrics;
import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit;

import android.app.Activity;
@@ -87,6 +88,7 @@ import androidx.window.extensions.embedding.TransactionManager.TransactionRecord
import androidx.window.extensions.layout.WindowLayoutComponentImpl;

import com.android.internal.annotations.VisibleForTesting;
import com.android.window.flags.Flags;

import java.util.ArrayList;
import java.util.Collections;
@@ -123,8 +125,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
     * and unregistered via {@link #clearSplitAttributesCalculator()}.
     * This is called when:
     * <ul>
     *   <li>{@link SplitPresenter#updateSplitContainer(SplitContainer, TaskFragmentContainer,
     *     WindowContainerTransaction)}</li>
     *   <li>{@link SplitPresenter#updateSplitContainer}</li>
     *   <li>There's a started Activity which matches {@link SplitPairRule} </li>
     *   <li>Checking whether the place holder should be launched if there's a Activity matches
     *   {@link SplitPlaceholderRule} </li>
@@ -759,6 +760,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
        if (targetContainer == null) {
            // When there is no embedding rule matched, try to place it in the top container
            // like a normal launch.
            // TODO(b/301034784): Check if it makes sense to place the activity in overlay
            //  container.
            targetContainer = taskContainer.getTopNonFinishingTaskFragmentContainer();
        }
        if (targetContainer == null) {
@@ -1007,6 +1010,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
        if (taskContainer == null) {
            return;
        }
        // TODO(b/301034784): Check if it makes sense to place the activity in overlay container.
        final TaskFragmentContainer targetContainer =
                taskContainer.getTopNonFinishingTaskFragmentContainer();
        if (targetContainer == null) {
@@ -1166,7 +1170,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity));
        if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer()
                && canReuseContainer(splitRule, splitContainer.getSplitRule(),
                        getTaskWindowMetrics(taskProperties.getConfiguration()),
                        taskProperties.getTaskMetrics(),
                        calculatedSplitAttributes, splitContainer.getCurrentSplitAttributes())) {
            // Can launch in the existing secondary container if the rules share the same
            // presentation.
@@ -1408,6 +1412,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
    private TaskFragmentContainer createEmptyExpandedContainer(
            @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId,
            @Nullable Activity launchingActivity) {
        return createEmptyContainer(wct, intent, taskId, new Rect(), launchingActivity,
                null /* overlayTag */);
    }

    /**
     * Returns an empty {@link TaskFragmentContainer} that we can launch an activity into.
     * If {@code overlayTag} is set, it means the created {@link TaskFragmentContainer} is an
     * overlay container.
     */
    @VisibleForTesting
    @GuardedBy("mLock")
    @Nullable
    TaskFragmentContainer createEmptyContainer(
            @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId,
            @NonNull Rect bounds, @Nullable Activity launchingActivity,
            @Nullable String overlayTag) {
        // We need an activity in the organizer process in the same Task to use as the owner
        // activity, as well as to get the Task window info.
        final Activity activityInTask;
@@ -1423,13 +1443,46 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
            // Can't find any activity in the Task that we can use as the owner activity.
            return null;
        }
        final TaskFragmentContainer expandedContainer = newContainer(intent, activityInTask,
                taskId);
        mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(),
                activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED);
        mPresenter.updateAnimationParams(wct, expandedContainer.getTaskFragmentToken(),
        final TaskFragmentContainer container = newContainer(null /* pendingAppearedActivity */,
                intent, activityInTask, taskId, null /* pairedPrimaryContainer*/, overlayTag);
        final IBinder taskFragmentToken = container.getTaskFragmentToken();
        // Note that taskContainer will not exist before calling #newContainer if the container
        // is the first embedded TF in the task.
        final TaskContainer taskContainer = container.getTaskContainer();
        final Rect taskBounds = taskContainer.getTaskProperties().getTaskMetrics().getBounds();
        final Rect sanitizedBounds = sanitizeBounds(bounds, intent, taskBounds);
        final int windowingMode = taskContainer
                .getWindowingModeForSplitTaskFragment(sanitizedBounds);
        mPresenter.createTaskFragment(wct, taskFragmentToken, activityInTask.getActivityToken(),
                sanitizedBounds, windowingMode);
        mPresenter.updateAnimationParams(wct, taskFragmentToken,
                TaskFragmentAnimationParams.DEFAULT);
        return expandedContainer;
        mPresenter.setTaskFragmentIsolatedNavigation(wct, taskFragmentToken,
                overlayTag != null && !sanitizedBounds.isEmpty());

        return container;
    }

    /**
     * Returns the expanded bounds if the {@code bounds} violate minimum dimension or are not fully
     * covered by the task bounds. Otherwise, returns {@code bounds}.
     */
    @NonNull
    private static Rect sanitizeBounds(@NonNull Rect bounds, @NonNull Intent intent,
                                       @NonNull Rect taskBounds) {
        if (bounds.isEmpty()) {
            // Don't need to check if the bounds follows the task bounds.
            return bounds;
        }
        if (boundsSmallerThanMinDimensions(bounds, getMinDimensions(intent))) {
            // Expand the bounds if the bounds are smaller than minimum dimensions.
            return new Rect();
        }
        if (!taskBounds.contains(bounds)) {
            // Expand the bounds if the bounds exceed the task bounds.
            return new Rect();
        }
        return bounds;
    }

    /**
@@ -1449,8 +1502,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
        final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer);
        final TaskContainer.TaskProperties taskProperties = mPresenter
                .getTaskProperties(primaryActivity);
        final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(
                taskProperties.getConfiguration());
        final WindowMetrics taskWindowMetrics = taskProperties.getTaskMetrics();
        final SplitAttributes calculatedSplitAttributes = mPresenter.computeSplitAttributes(
                taskProperties, splitRule, splitRule.getDefaultSplitAttributes(),
                getActivityIntentMinDimensionsPair(primaryActivity, intent));
@@ -1519,14 +1571,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
    TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity,
            @NonNull Activity activityInTask, int taskId) {
        return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */,
                activityInTask, taskId, null /* pairedPrimaryContainer */);
                activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */);
    }

    @GuardedBy("mLock")
    TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent,
            @NonNull Activity activityInTask, int taskId) {
        return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
                activityInTask, taskId, null /* pairedPrimaryContainer */);
                activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */);
    }

    @GuardedBy("mLock")
    TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent,
                                       @NonNull Activity activityInTask, int taskId,
                                       @NonNull TaskFragmentContainer pairedPrimaryContainer) {
        return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
                activityInTask, taskId, pairedPrimaryContainer, null /* tag */);
    }

    /**
@@ -1540,11 +1600,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
     * @param taskId                    parent Task of the new TaskFragment.
     * @param pairedPrimaryContainer    the paired primary {@link TaskFragmentContainer}. When it is
     *                                  set, the new container will be added right above it.
     * @param overlayTag                The tag for the new created overlay container. It must be
     *                                  needed if {@code isOverlay} is {@code true}. Otherwise,
     *                                  it should be {@code null}.
     */
    @GuardedBy("mLock")
    TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity,
            @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId,
            @Nullable TaskFragmentContainer pairedPrimaryContainer) {
            @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag) {
        if (activityInTask == null) {
            throw new IllegalArgumentException("activityInTask must not be null,");
        }
@@ -1553,7 +1616,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
        }
        final TaskContainer taskContainer = mTaskContainers.get(taskId);
        final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity,
                pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer);
                pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer, overlayTag);
        return container;
    }

@@ -1754,13 +1817,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
     * Updates {@link SplitContainer} with the given {@link SplitAttributes} if the
     * {@link SplitContainer} is the top most and not finished. If passed {@link SplitAttributes}
     * are {@code null}, the {@link SplitAttributes} will be calculated with
     * {@link SplitPresenter#computeSplitAttributes(TaskContainer.TaskProperties, SplitRule, Pair)}.
     * {@link SplitPresenter#computeSplitAttributes}.
     *
     * @param splitContainer The {@link SplitContainer} to update
     * @param splitAttributes Update with this {@code splitAttributes} if it is not {@code null}.
     *                        Otherwise, use the value calculated by
     *                        {@link SplitPresenter#computeSplitAttributes(
     *                        TaskContainer.TaskProperties, SplitRule, Pair)}
     *                        {@link SplitPresenter#computeSplitAttributes}
     *
     * @return {@code true} if the update succeed. Otherwise, returns {@code false}.
     */
@@ -2255,6 +2317,96 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
        return shouldRetainAssociatedContainer(finishingContainer, associatedContainer);
    }

    /**
     * Gets all overlay containers from all tasks in this process, or an empty list if there's
     * no overlay container.
     * <p>
     * Note that we only support one overlay container for each task, but an app could have multiple
     * tasks.
     */
    @VisibleForTesting
    @GuardedBy("mLock")
    @NonNull
    List<TaskFragmentContainer> getAllOverlayTaskFragmentContainers() {
        final List<TaskFragmentContainer> overlayContainers = new ArrayList<>();
        for (int i = 0; i < mTaskContainers.size(); i++) {
            final TaskContainer taskContainer = mTaskContainers.valueAt(i);
            final TaskFragmentContainer overlayContainer = taskContainer.getOverlayContainer();
            if (overlayContainer != null) {
                overlayContainers.add(overlayContainer);
            }
        }
        return overlayContainers;
    }

    @VisibleForTesting
    // Suppress GuardedBy warning because lint ask to mark this method as
    // @GuardedBy(container.mController.mLock), which is mLock itself
    @SuppressWarnings("GuardedBy")
    @GuardedBy("mLock")
    @Nullable
    TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(
            @NonNull WindowContainerTransaction wct,
            @NonNull OverlayCreateParams overlayCreateParams, int launchTaskId,
            @NonNull Intent intent, @NonNull Activity launchActivity) {
        final int taskId = overlayCreateParams.getTaskId();
        if (taskId != launchTaskId) {
            // The task ID doesn't match the launch activity's. Cannot determine the host task
            // to launch the overlay.
            throw new IllegalArgumentException("The task ID of "
                    + "OverlayCreateParams#launchingActivity must match the task ID of "
                    + "the activity to #startActivity with the activity options that takes "
                    + "OverlayCreateParams.");
        }
        final List<TaskFragmentContainer> overlayContainers =
                getAllOverlayTaskFragmentContainers();
        final String overlayTag = overlayCreateParams.getTag();

        // If the requested bounds of OverlayCreateParams are smaller than minimum dimensions
        // specified by Intent, expand the overlay container to fill the parent task instead.
        final Rect bounds = overlayCreateParams.getBounds();
        final Size minDimensions = getMinDimensions(intent);
        final boolean shouldExpandContainer = boundsSmallerThanMinDimensions(bounds,
                minDimensions);
        if (!overlayContainers.isEmpty()) {
            for (final TaskFragmentContainer overlayContainer : overlayContainers) {
                if (!overlayTag.equals(overlayContainer.getOverlayTag())
                        && taskId == overlayContainer.getTaskId()) {
                    // If there's an overlay container with different tag shown in the same
                    // task, dismiss the existing overlay container.
                    overlayContainer.finish(false /* shouldFinishDependant */, mPresenter,
                            wct, SplitController.this);
                }
                if (overlayTag.equals(overlayContainer.getOverlayTag())
                        && taskId != overlayContainer.getTaskId()) {
                    // If there's an overlay container with same tag in a different task,
                    // dismiss the overlay container since the tag must be unique per process.
                    overlayContainer.finish(false /* shouldFinishDependant */, mPresenter,
                            wct, SplitController.this);
                }
                if (overlayTag.equals(overlayContainer.getOverlayTag())
                        && taskId == overlayContainer.getTaskId()) {
                    // If there's an overlay container with the same tag and task ID, we treat
                    // the OverlayCreateParams as the update to the container.
                    final Rect taskBounds = overlayContainer.getTaskContainer().getTaskProperties()
                            .getTaskMetrics().getBounds();
                    final IBinder overlayToken = overlayContainer.getTaskFragmentToken();
                    final Rect sanitizedBounds = sanitizeBounds(bounds, intent, taskBounds);
                    mPresenter.resizeTaskFragment(wct, overlayToken, sanitizedBounds);
                    mPresenter.setTaskFragmentIsolatedNavigation(wct, overlayToken,
                            !sanitizedBounds.isEmpty());
                    // We can just return the updated overlay container and don't need to
                    // check other condition since we only have one OverlayCreateParams, and
                    // if the tag and task are matched, it's impossible to match another task
                    // or tag since tags and tasks are all unique.
                    return overlayContainer;
                }
            }
        }
        return createEmptyContainer(wct, intent, taskId,
                (shouldExpandContainer ? new Rect() : bounds), launchActivity, overlayTag);
    }

    private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter {

        @Override
@@ -2417,8 +2569,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                final TaskFragmentContainer launchedInTaskFragment;
                if (launchingActivity != null) {
                    final int taskId = getTaskId(launchingActivity);
                    final OverlayCreateParams overlayCreateParams =
                            OverlayCreateParams.fromBundle(options);
                    if (Flags.activityEmbeddingOverlayPresentationFlag()
                            && overlayCreateParams != null) {
                        launchedInTaskFragment = createOrUpdateOverlayTaskFragmentIfNeeded(wct,
                                overlayCreateParams, taskId, intent, launchingActivity);
                    } else {
                        launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent,
                                launchingActivity);
                    }
                } else {
                    launchedInTaskFragment = resolveStartActivityIntentFromNonActivityContext(wct,
                            intent);
+9 −15
Original line number Diff line number Diff line
@@ -30,12 +30,10 @@ import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
import android.util.DisplayMetrics;
import android.util.LayoutDirection;
import android.util.Pair;
import android.util.Size;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowMetrics;
import android.window.TaskFragmentAnimationParams;
import android.window.TaskFragmentCreationParams;
@@ -307,8 +305,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
        }

        final int taskId = primaryContainer.getTaskId();
        final TaskFragmentContainer secondaryContainer = mController.newContainer(
                null /* pendingAppearedActivity */, activityIntent, launchingActivity, taskId,
        final TaskFragmentContainer secondaryContainer = mController.newContainer(activityIntent,
                launchingActivity, taskId,
                // Pass in the primary container to make sure it is added right above the primary.
                primaryContainer);
        final TaskContainer taskContainer = mController.getTaskContainer(taskId);
@@ -618,7 +616,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
            @NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes,
            @Nullable Pair<Size, Size> minDimensionsPair) {
        final Configuration taskConfiguration = taskProperties.getConfiguration();
        final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration);
        final WindowMetrics taskWindowMetrics = taskProperties.getTaskMetrics();
        final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
                mController.getSplitAttributesCalculator();
        final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics);
@@ -713,11 +711,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
        return new Size(windowLayout.minWidth, windowLayout.minHeight);
    }

    private static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds,
    static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds,
            @Nullable Size minDimensions) {
        if (minDimensions == null) {
            return false;
        }
        // Empty bounds mean the bounds follow the parent host task's bounds. Skip the check.
        if (bounds.isEmpty()) {
            return false;
        }
        return bounds.width() < minDimensions.getWidth()
                || bounds.height() < minDimensions.getHeight();
    }
@@ -1066,14 +1068,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {

    @NonNull
    WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) {
        return getTaskWindowMetrics(getTaskProperties(activity).getConfiguration());
    }

    @NonNull
    static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) {
        final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds();
        // TODO(b/190433398): Supply correct insets.
        final float density = taskConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
        return new WindowMetrics(taskBounds, WindowInsets.CONSUMED, density);
        return getTaskProperties(activity).getTaskMetrics();
    }
}
+59 −6

File changed.

Preview size limit exceeded, changes collapsed.

+35 −1

File changed.

Preview size limit exceeded, changes collapsed.

Loading