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

Commit 67a2b849 authored by Neil Fuller's avatar Neil Fuller Committed by Android (Google) Code Review
Browse files

Merge "Centralize upper/lower time bound logic"

parents 052e1784 13415197
Loading
Loading
Loading
Loading
+158 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 android.app.timedetector;

import android.annotation.NonNull;
import android.os.Build;

import java.time.Instant;

/**
 * A utility class for fundamental time detector-related logic that doesn't need to communicate with
 * the time detector service, i.e. because it can use SDK APIs or hard-coded facts, and doesn't need
 * permissions or singleton state. Putting logic here avoids the need to expose binder-based calls
 * or to duplicate code to share related logic (since android.app.timedetector classes are visible
 * to all processes).
 *
 * @hide
 */
// Not final for easy replacement / mocking during tests.
public class TimeDetectorHelper {

    /**
     * See {@link #getManualDateSelectionYearMin()}. Chosen to produce Unix epoch times be greater
     * than {@link #MANUAL_SUGGESTION_LOWER_BOUND}.
     */
    private static final int MANUAL_SUGGESTION_YEAR_MIN = 2008;

    /**
     * The maximum gregorian calendar year to allow for manual date selection on devices unlikely to
     * have Y2038 issues. This serves as a sensible UI-enforced limit though the system server may
     * support a larger upper bound. Users besides future archeologists are unlikely to need higher
     * values, for a few years at least.
     */
    private static final int MANUAL_SUGGESTION_YEAR_MAX_WITHOUT_Y2038_ISSUE = 2100;

    /**
     * The maximum gregorian calendar year to allow for manual date selection on devices that may
     * have Y2038 issues. This serves as a sensible UI-enforced limit though the system server may
     * support a larger upper bound. That is, the signed 32-bit milliseconds value is
     * 03:14:07 UTC on 19 January 2038, but this constant means users can only enter dates up to
     * 2037-12-31. See {@link #MANUAL_SUGGESTION_YEAR_MAX_WITH_Y2038_ISSUE}.
     *
     * <p>Note: This UI limit also doesn't prevent devices reaching the Y2038 roll-over time through
     * the natural passage of time, it just prevents users potentially causing issues in the years
     * leading up to it accidentally via the UI.
     */
    private static final int MANUAL_SUGGESTION_YEAR_MAX_WITH_Y2038_ISSUE = 2037;

    /**
     * The upper bound for valid suggestions when the Y2038 issue is a risk. This is the instant
     * when the Y2038 issue occurs.
     */
    private static final Instant SUGGESTION_UPPER_BOUND_WITH_Y2038_ISSUE =
            Instant.ofEpochMilli(1000L * Integer.MAX_VALUE);

    /**
     * The upper bound for valid suggestions when the Y2038 issue is not a risk. This values means
     * there is no practical upper bound.
     *
     * <p>Make sure this value remains in the value representable as a signed int64 Unix epoch
     * millis value as in various places {@link Instant#toEpochMilli()} is called, and that throws
     * an exception if the value is too large.
     */
    private static final Instant SUGGESTION_UPPER_BOUND_WIITHOUT_Y2038_ISSUE =
            Instant.ofEpochMilli(Long.MAX_VALUE);

    /** See {@link #getManualSuggestionLowerBound()}. */
    private static final Instant MANUAL_SUGGESTION_LOWER_BOUND =
            Instant.ofEpochMilli(1194220800000L); // Nov 5, 2007, 0:00 UTC

    /**
     * The lowest value in Unix epoch milliseconds that is considered a valid automatic suggestion.
     * See also {@link #MANUAL_SUGGESTION_LOWER_BOUND}.
     *
     * <p>Note that this is a default value. The lower value enforced can be overridden to be
     * lower in the system server with flags for testing.
     */
    private static final Instant AUTO_SUGGESTION_LOWER_BOUND_DEFAULT = Instant.ofEpochMilli(
            Long.max(android.os.Environment.getRootDirectory().lastModified(), Build.TIME));

    /** The singleton instance of this class. */
    public static final TimeDetectorHelper INSTANCE = new TimeDetectorHelper();

    /** Constructor present for subclassing in tests. Use {@link #INSTANCE} in production code. */
    protected TimeDetectorHelper() {}

    /**
     * Returns the minimum gregorian calendar year to offer for manual date selection. This serves
     * as a sensible UI-enforced lower limit, the system server may support a smaller lower bound.
     */
    public int getManualDateSelectionYearMin() {
        return MANUAL_SUGGESTION_YEAR_MIN;
    }

    /**
     * Returns the maximum gregorian calendar year to offer for manual date selection. This serves
     * as a sensible UI-enforced lower limit, the system server may support a larger upper bound.
     */
    public int getManualDateSelectionYearMax() {
        return getDeviceHasY2038Issue()
                ? MANUAL_SUGGESTION_YEAR_MAX_WITH_Y2038_ISSUE
                : MANUAL_SUGGESTION_YEAR_MAX_WITHOUT_Y2038_ISSUE;
    }

    /**
     * Returns the lowest value in Unix epoch milliseconds that is considered a valid manual
     * suggestion. For historical reasons Android has a different lower limit for manual input than
     * automatic. This may change in the future to align with automatic suggestions, but has been
     * kept initially to avoid breaking manual tests that are hard-coded with old dates real users
     * will never want to use.
     */
    @NonNull
    public Instant getManualSuggestionLowerBound() {
        return MANUAL_SUGGESTION_LOWER_BOUND;
    }

    /**
     * Returns the lowest value in Unix epoch milliseconds that is considered a valid automatic
     * suggestion. See also {@link #MANUAL_SUGGESTION_LOWER_BOUND}.
     *
     * <p>Note that this is a default value. The lower value enforced can be overridden to be
     * different in the system server with server flags.
     */
    @NonNull
    public Instant getAutoSuggestionLowerBoundDefault() {
        return AUTO_SUGGESTION_LOWER_BOUND_DEFAULT;
    }

    /** Returns the upper bound to enforce for all time suggestions (manual and automatic). */
    @NonNull
    public Instant getSuggestionUpperBound() {
        return getDeviceHasY2038Issue()
                ? SUGGESTION_UPPER_BOUND_WITH_Y2038_ISSUE
                : SUGGESTION_UPPER_BOUND_WIITHOUT_Y2038_ISSUE;
    }

    /**
     * Returns {@code true} if the device may be at risk of time_t overflow (because bionic
     * defines time_t as a 32-bit signed integer for 32-bit processes).
     */
    private boolean getDeviceHasY2038Issue() {
        return Build.SUPPORTED_32_BIT_ABIS.length > 0;
    }
}
+63 −39
Original line number Diff line number Diff line
@@ -46,9 +46,10 @@ public final class ConfigurationInternal {

    private final boolean mAutoDetectionSupported;
    private final int mSystemClockUpdateThresholdMillis;
    private final Instant mAutoTimeLowerBound;
    private final Instant mAutoSuggestionLowerBound;
    private final Instant mManualSuggestionLowerBound;
    private final Instant mSuggestionUpperBound;
    private final @Origin int[] mOriginPriorities;
    private final boolean mDeviceHasY2038Issue;
    private final boolean mAutoDetectionEnabledSetting;
    private final @UserIdInt int mUserId;
    private final boolean mUserConfigAllowed;
@@ -56,9 +57,10 @@ public final class ConfigurationInternal {
    private ConfigurationInternal(Builder builder) {
        mAutoDetectionSupported = builder.mAutoDetectionSupported;
        mSystemClockUpdateThresholdMillis = builder.mSystemClockUpdateThresholdMillis;
        mAutoTimeLowerBound = Objects.requireNonNull(builder.mAutoTimeLowerBound);
        mAutoSuggestionLowerBound = Objects.requireNonNull(builder.mAutoSuggestionLowerBound);
        mManualSuggestionLowerBound = Objects.requireNonNull(builder.mManualSuggestionLowerBound);
        mSuggestionUpperBound = Objects.requireNonNull(builder.mSuggestionUpperBound);
        mOriginPriorities = Objects.requireNonNull(builder.mOriginPriorities);
        mDeviceHasY2038Issue = builder.mDeviceHasY2038Issue;
        mAutoDetectionEnabledSetting = builder.mAutoDetectionEnabledSetting;

        mUserId = builder.mUserId;
@@ -80,30 +82,39 @@ public final class ConfigurationInternal {
    }

    /**
     * Returns the lower bound for valid automatic times. It is guaranteed to be in the past,
     * i.e. it is unrelated to the current system clock time.
     * Returns the lower bound for valid automatic time suggestions. It is guaranteed to be in the
     * past, i.e. it is unrelated to the current system clock time.
     * It holds no other meaning; it could be related to when the device system image was built,
     * or could be updated by a mainline module.
     */
    @NonNull
    public Instant getAutoTimeLowerBound() {
        return mAutoTimeLowerBound;
    public Instant getAutoSuggestionLowerBound() {
        return mAutoSuggestionLowerBound;
    }

    /**
     * Returns the order to look at time suggestions when automatically detecting time.
     * See {@code #ORIGIN_} constants
     * Returns the lower bound for valid manual time suggestions. It is guaranteed to be in the
     * past, i.e. it is unrelated to the current system clock time.
     */
    public @Origin int[] getAutoOriginPriorities() {
        return mOriginPriorities;
    @NonNull
    public Instant getManualSuggestionLowerBound() {
        return mManualSuggestionLowerBound;
    }

    /**
     * Returns {@code true} if the device may be at risk of time_t overflow (because bionic
     * defines time_t as a 32-bit signed integer for 32-bit processes).
     * Returns the upper bound for valid time suggestions (manual and automatic).
     */
    public boolean getDeviceHasY2038Issue() {
        return mDeviceHasY2038Issue;
    @NonNull
    public Instant getSuggestionUpperBound() {
        return mSuggestionUpperBound;
    }

    /**
     * Returns the order to look at time suggestions when automatically detecting time.
     * See {@code #ORIGIN_} constants
     */
    public @Origin int[] getAutoOriginPriorities() {
        return mOriginPriorities;
    }

    /** Returns the value of the auto time detection enabled setting. */
@@ -207,16 +218,17 @@ public final class ConfigurationInternal {
                && mAutoDetectionEnabledSetting == that.mAutoDetectionEnabledSetting
                && mUserId == that.mUserId && mUserConfigAllowed == that.mUserConfigAllowed
                && mSystemClockUpdateThresholdMillis == that.mSystemClockUpdateThresholdMillis
                && mAutoTimeLowerBound.equals(that.mAutoTimeLowerBound)
                && mDeviceHasY2038Issue == that.mDeviceHasY2038Issue
                && mAutoSuggestionLowerBound.equals(that.mAutoSuggestionLowerBound)
                && mManualSuggestionLowerBound.equals(that.mManualSuggestionLowerBound)
                && mSuggestionUpperBound.equals(that.mSuggestionUpperBound)
                && Arrays.equals(mOriginPriorities, that.mOriginPriorities);
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(mAutoDetectionSupported, mAutoDetectionEnabledSetting, mUserId,
                mUserConfigAllowed, mSystemClockUpdateThresholdMillis, mAutoTimeLowerBound,
                mDeviceHasY2038Issue);
                mUserConfigAllowed, mSystemClockUpdateThresholdMillis, mAutoSuggestionLowerBound,
                mManualSuggestionLowerBound, mSuggestionUpperBound);
        result = 31 * result + Arrays.hashCode(mOriginPriorities);
        return result;
    }
@@ -230,10 +242,13 @@ public final class ConfigurationInternal {
        return "ConfigurationInternal{"
                + "mAutoDetectionSupported=" + mAutoDetectionSupported
                + ", mSystemClockUpdateThresholdMillis=" + mSystemClockUpdateThresholdMillis
                + ", mAutoTimeLowerBound=" + mAutoTimeLowerBound
                + "(" + mAutoTimeLowerBound.toEpochMilli() + ")"
                + ", mAutoSuggestionLowerBound=" + mAutoSuggestionLowerBound
                + "(" + mAutoSuggestionLowerBound.toEpochMilli() + ")"
                + ", mManualSuggestionLowerBound=" + mManualSuggestionLowerBound
                + "(" + mManualSuggestionLowerBound.toEpochMilli() + ")"
                + ", mSuggestionUpperBound=" + mSuggestionUpperBound
                + "(" + mSuggestionUpperBound.toEpochMilli() + ")"
                + ", mOriginPriorities=" + originPrioritiesString
                + ", mDeviceHasY2038Issue=" + mDeviceHasY2038Issue
                + ", mAutoDetectionEnabled=" + mAutoDetectionEnabledSetting
                + ", mUserId=" + mUserId
                + ", mUserConfigAllowed=" + mUserConfigAllowed
@@ -243,9 +258,10 @@ public final class ConfigurationInternal {
    static final class Builder {
        private boolean mAutoDetectionSupported;
        private int mSystemClockUpdateThresholdMillis;
        @NonNull private Instant mAutoTimeLowerBound;
        @NonNull private Instant mAutoSuggestionLowerBound;
        @NonNull private Instant mManualSuggestionLowerBound;
        @NonNull private Instant mSuggestionUpperBound;
        @NonNull private @Origin int[] mOriginPriorities;
        private boolean mDeviceHasY2038Issue;
        private boolean mAutoDetectionEnabledSetting;

        private final @UserIdInt int mUserId;
@@ -263,9 +279,10 @@ public final class ConfigurationInternal {
            this.mUserConfigAllowed = toCopy.mUserConfigAllowed;
            this.mAutoDetectionSupported = toCopy.mAutoDetectionSupported;
            this.mSystemClockUpdateThresholdMillis = toCopy.mSystemClockUpdateThresholdMillis;
            this.mAutoTimeLowerBound = toCopy.mAutoTimeLowerBound;
            this.mAutoSuggestionLowerBound = toCopy.mAutoSuggestionLowerBound;
            this.mManualSuggestionLowerBound = toCopy.mManualSuggestionLowerBound;
            this.mSuggestionUpperBound = toCopy.mSuggestionUpperBound;
            this.mOriginPriorities = toCopy.mOriginPriorities;
            this.mDeviceHasY2038Issue = toCopy.mDeviceHasY2038Issue;
            this.mAutoDetectionEnabledSetting = toCopy.mAutoDetectionEnabledSetting;
        }

@@ -296,10 +313,26 @@ public final class ConfigurationInternal {
        }

        /**
         * Sets the lower bound for valid automatic times.
         * Sets the lower bound for valid automatic time suggestions.
         */
        public Builder setAutoTimeLowerBound(@NonNull Instant autoTimeLowerBound) {
            mAutoTimeLowerBound = Objects.requireNonNull(autoTimeLowerBound);
        public Builder setAutoSuggestionLowerBound(@NonNull Instant autoSuggestionLowerBound) {
            mAutoSuggestionLowerBound = Objects.requireNonNull(autoSuggestionLowerBound);
            return this;
        }

        /**
         * Sets the lower bound for valid manual time suggestions.
         */
        public Builder setManualSuggestionLowerBound(@NonNull Instant manualSuggestionLowerBound) {
            mManualSuggestionLowerBound = Objects.requireNonNull(manualSuggestionLowerBound);
            return this;
        }

        /**
         * Sets the upper bound for valid time suggestions (manual and automatic).
         */
        public Builder setSuggestionUpperBound(@NonNull Instant suggestionUpperBound) {
            mSuggestionUpperBound = Objects.requireNonNull(suggestionUpperBound);
            return this;
        }

@@ -320,15 +353,6 @@ public final class ConfigurationInternal {
            return this;
        }

        /**
         * Returns {@code true} if the device may be at risk of time_t overflow (because bionic
         * defines time_t as a 32-bit signed integer for 32-bit processes).
         */
        Builder setDeviceHasY2038Issue(boolean deviceHasY2038Issue) {
            mDeviceHasY2038Issue = deviceHasY2038Issue;
            return this;
        }

        /** Returns a new {@link ConfigurationInternal}. */
        @NonNull
        ConfigurationInternal build() {
+7 −16
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.app.ActivityManagerInternal;
import android.app.time.TimeCapabilities;
import android.app.time.TimeCapabilitiesAndConfig;
import android.app.time.TimeConfiguration;
import android.app.timedetector.TimeDetectorHelper;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
@@ -38,7 +39,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.os.Build;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
@@ -74,13 +74,6 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor {
    private static final @Origin int[]
            DEFAULT_AUTOMATIC_TIME_ORIGIN_PRIORITIES = { ORIGIN_TELEPHONY, ORIGIN_NETWORK };

    /**
     * Time in the past. If an automatic time suggestion is before this point, it is sure to be
     * incorrect.
     */
    private static final Instant TIME_LOWER_BOUND_DEFAULT = Instant.ofEpochMilli(
            Long.max(android.os.Environment.getRootDirectory().lastModified(), Build.TIME));

    /** Device config keys that affect the {@link TimeDetectorService}. */
    private static final Set<String> SERVER_FLAGS_KEYS_TO_WATCH = Set.of(
            KEY_TIME_DETECTOR_LOWER_BOUND_MILLIS_OVERRIDE,
@@ -237,14 +230,16 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor {
    @Override
    @NonNull
    public synchronized ConfigurationInternal getConfigurationInternal(@UserIdInt int userId) {
        TimeDetectorHelper timeDetectorHelper = TimeDetectorHelper.INSTANCE;
        return new ConfigurationInternal.Builder(userId)
                .setUserConfigAllowed(isUserConfigAllowed(userId))
                .setAutoDetectionSupported(isAutoDetectionSupported())
                .setAutoDetectionEnabledSetting(getAutoDetectionEnabledSetting())
                .setSystemClockUpdateThresholdMillis(getSystemClockUpdateThresholdMillis())
                .setAutoTimeLowerBound(getAutoTimeLowerBound())
                .setAutoSuggestionLowerBound(getAutoSuggestionLowerBound())
                .setManualSuggestionLowerBound(timeDetectorHelper.getManualSuggestionLowerBound())
                .setSuggestionUpperBound(timeDetectorHelper.getSuggestionUpperBound())
                .setOriginPriorities(getOriginPriorities())
                .setDeviceHasY2038Issue(getDeviceHasY2038Issue())
                .build();
    }

@@ -291,9 +286,9 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor {
    }

    @NonNull
    private Instant getAutoTimeLowerBound() {
    private Instant getAutoSuggestionLowerBound() {
        return mServerFlags.getOptionalInstant(KEY_TIME_DETECTOR_LOWER_BOUND_MILLIS_OVERRIDE)
                .orElse(TIME_LOWER_BOUND_DEFAULT);
                .orElse(TimeDetectorHelper.INSTANCE.getAutoSuggestionLowerBoundDefault());
    }

    @NonNull
@@ -310,10 +305,6 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor {
        return DEFAULT_AUTOMATIC_TIME_ORIGIN_PRIORITIES;
    }

    private boolean getDeviceHasY2038Issue() {
        return Build.SUPPORTED_32_BIT_ABIS.length > 0;
    }

    /**
     * A base supplier of an array of time origin integers in priority order.
     * It handles memoization of the result to avoid repeated string parsing when nothing has
+29 −12
Original line number Diff line number Diff line
@@ -84,9 +84,6 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
     */
    private static final int KEEP_SUGGESTION_HISTORY_SIZE = 10;

    /** The value in Unix epoch milliseconds of the Y2038 issue. */
    private static final long Y2038_LIMIT_IN_MILLIS = 1000L * Integer.MAX_VALUE;

    /**
     * A log that records the decisions / decision metadata that affected the device's system clock
     * time. This is logged in bug reports to assist with debugging issues with detection.
@@ -248,7 +245,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {

        final TimestampedValue<Long> newUnixEpochTime = suggestion.getUnixEpochTime();

        if (!validateSuggestionTime(newUnixEpochTime, suggestion)) {
        if (!validateManualSuggestionTime(newUnixEpochTime, suggestion)) {
            return false;
        }

@@ -424,7 +421,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
    }

    @GuardedBy("this")
    private boolean validateSuggestionTime(
    private boolean validateSuggestionCommon(
            @NonNull TimestampedValue<Long> newUnixEpochTime, @NonNull Object suggestion) {
        if (newUnixEpochTime.getValue() == null) {
            Slog.w(LOG_TAG, "Suggested time value is null. suggestion=" + suggestion);
@@ -441,8 +438,8 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
            return false;
        }

        if (newUnixEpochTime.getValue() > Y2038_LIMIT_IN_MILLIS
                && mCurrentConfigurationInternal.getDeviceHasY2038Issue()) {
        if (newUnixEpochTime.getValue()
                > mCurrentConfigurationInternal.getSuggestionUpperBound().toEpochMilli()) {
            // This check won't prevent a device's system clock exceeding Integer.MAX_VALUE Unix
            // seconds through the normal passage of time, but it will stop it jumping above 2038
            // because of a "bad" suggestion. b/204193177
@@ -453,20 +450,40 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy {
        return true;
    }

    /**
     * Returns {@code true} if an automatic time suggestion time is valid.
     * See also {@link #validateManualSuggestionTime(TimestampedValue, Object)}.
     */
    @GuardedBy("this")
    private boolean validateAutoSuggestionTime(
            @NonNull TimestampedValue<Long> newUnixEpochTime, @NonNull Object suggestion)  {
        return validateSuggestionTime(newUnixEpochTime, suggestion)
                && validateSuggestionAgainstLowerBound(newUnixEpochTime, suggestion);
        Instant lowerBound = mCurrentConfigurationInternal.getAutoSuggestionLowerBound();
        return validateSuggestionCommon(newUnixEpochTime, suggestion)
                && validateSuggestionAgainstLowerBound(newUnixEpochTime, suggestion,
                lowerBound);
    }

    /**
     * Returns {@code true} if a manual time suggestion time is valid.
     * See also {@link #validateAutoSuggestionTime(TimestampedValue, Object)}.
     */
    @GuardedBy("this")
    private boolean validateSuggestionAgainstLowerBound(
    private boolean validateManualSuggestionTime(
            @NonNull TimestampedValue<Long> newUnixEpochTime, @NonNull Object suggestion)  {
        Instant lowerBound = mCurrentConfigurationInternal.getAutoTimeLowerBound();
        Instant lowerBound = mCurrentConfigurationInternal.getManualSuggestionLowerBound();

        // Suggestion is definitely wrong if it comes before lower time bound.
        return validateSuggestionCommon(newUnixEpochTime, suggestion)
                && validateSuggestionAgainstLowerBound(newUnixEpochTime, suggestion, lowerBound);
    }

    @GuardedBy("this")
    private boolean validateSuggestionAgainstLowerBound(
            @NonNull TimestampedValue<Long> newUnixEpochTime, @NonNull Object suggestion,
            @NonNull Instant lowerBound) {

        // Suggestion is definitely wrong if it comes before lower time bound.
        if (lowerBound.isAfter(Instant.ofEpochMilli(newUnixEpochTime.getValue()))) {
        if (lowerBound.toEpochMilli() > newUnixEpochTime.getValue()) {
            Slog.w(LOG_TAG, "Suggestion points to time before lower bound, skipping it. "
                    + "suggestion=" + suggestion + ", lower bound=" + lowerBound);
            return false;
+12 −7

File changed.

Preview size limit exceeded, changes collapsed.

Loading