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

Commit 08ed0246 authored by Santos Cordon's avatar Santos Cordon Committed by Automerger Merge Worker
Browse files

Merge "Specify refresh rate limits for sensors in config." into sc-dev am: fa515155

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14690890

Change-Id: I16423a772ab80044e45e43974a17bb4497e9be71
parents 6619740e fa515155
Loading
Loading
Loading
Loading
+86 −0
Original line number Diff line number Diff line
@@ -22,12 +22,15 @@ import android.hardware.SensorManager;
import android.os.Handler;
import android.os.PowerManager;
import android.util.IntArray;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;

import java.util.Objects;

/**
 * Display manager local system service interface.
 *
@@ -292,6 +295,23 @@ public abstract class DisplayManagerInternal {
    @DisplayManager.SwitchingType
    public abstract int getRefreshRateSwitchingType();

    /**
     * Return the refresh rate restriction for the specified display and sensor pairing. If the
     * specified sensor is identified as an associated sensor in the specified display's
     * display-device-config file, then return any refresh rate restrictions that it might specify.
     * If no restriction is specified, or the sensor is not associated with the display, then null
     * will be returned.
     *
     * @param displayId The display to check against.
     * @param name The name of the sensor.
     * @param type The type of sensor.
     *
     * @return The min/max refresh-rate restriction as a {@link Pair} of floats, or null if not
     * restricted.
     */
    public abstract RefreshRateRange getRefreshRateForDisplayAndSensor(
            int displayId, String name, String type);

    /**
     * Describes the requested power state of the display.
     *
@@ -527,4 +547,70 @@ public abstract class DisplayManagerInternal {
         */
        void onDisplayGroupChanged(int groupId);
    }

    /**
     * Information about the min and max refresh rate DM would like to set the display to.
     */
    public static final class RefreshRateRange {
        public static final String TAG = "RefreshRateRange";

        // The tolerance within which we consider something approximately equals.
        public static final float FLOAT_TOLERANCE = 0.01f;

        /**
         * The lowest desired refresh rate.
         */
        public float min;

        /**
         * The highest desired refresh rate.
         */
        public float max;

        public RefreshRateRange() {}

        public RefreshRateRange(float min, float max) {
            if (min < 0 || max < 0 || min > max + FLOAT_TOLERANCE) {
                Slog.e(TAG, "Wrong values for min and max when initializing RefreshRateRange : "
                        + min + " " + max);
                this.min = this.max = 0;
                return;
            }
            if (min > max) {
                // Min and max are within epsilon of each other, but in the wrong order.
                float t = min;
                min = max;
                max = t;
            }
            this.min = min;
            this.max = max;
        }

        /**
         * Checks whether the two objects have the same values.
         */
        @Override
        public boolean equals(Object other) {
            if (other == this) {
                return true;
            }

            if (!(other instanceof RefreshRateRange)) {
                return false;
            }

            RefreshRateRange refreshRateRange = (RefreshRateRange) other;
            return (min == refreshRateRange.min && max == refreshRateRange.max);
        }

        @Override
        public int hashCode() {
            return Objects.hash(min, max);
        }

        @Override
        public String toString() {
            return "(" + min + " " + max + ")";
        }
    }
}
+46 −14
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.os.Environment;
import android.os.PowerManager;
import android.text.TextUtils;
import android.util.MathUtils;
import android.util.Slog;
import android.util.Spline;
@@ -34,6 +35,7 @@ import com.android.server.display.config.HbmTiming;
import com.android.server.display.config.HighBrightnessMode;
import com.android.server.display.config.NitsMap;
import com.android.server.display.config.Point;
import com.android.server.display.config.RefreshRateRange;
import com.android.server.display.config.SensorDetails;
import com.android.server.display.config.XmlParser;

@@ -79,10 +81,10 @@ public class DisplayDeviceConfig {
    private final Context mContext;

    // The details of the ambient light sensor associated with this display.
    private final SensorIdentifier mAmbientLightSensor = new SensorIdentifier();
    private final SensorData mAmbientLightSensor = new SensorData();

    // The details of the proximity sensor associated with this display.
    private final SensorIdentifier mProximitySensor = new SensorIdentifier();
    private final SensorData mProximitySensor = new SensorData();

    // Nits and backlight values that are loaded from either the display device config file, or
    // config.xml. These are the raw values and just used for the dumpsys
@@ -113,6 +115,7 @@ public class DisplayDeviceConfig {
    private List<String> mQuirks;
    private boolean mIsHighBrightnessModeEnabled = false;
    private HighBrightnessModeData mHbmData;
    private String mLoadedFrom = null;

    private DisplayDeviceConfig(Context context) {
        mContext = context;
@@ -273,11 +276,11 @@ public class DisplayDeviceConfig {
        return mBrightnessRampSlowIncrease;
    }

    SensorIdentifier getAmbientLightSensor() {
    SensorData getAmbientLightSensor() {
        return mAmbientLightSensor;
    }

    SensorIdentifier getProximitySensor() {
    SensorData getProximitySensor() {
        return mProximitySensor;
    }

@@ -306,7 +309,8 @@ public class DisplayDeviceConfig {
    @Override
    public String toString() {
        String str = "DisplayDeviceConfig{"
                + "mBacklight=" + Arrays.toString(mBacklight)
                + "mLoadedFrom=" + mLoadedFrom
                + ", mBacklight=" + Arrays.toString(mBacklight)
                + ", mNits=" + Arrays.toString(mNits)
                + ", mRawBacklight=" + Arrays.toString(mRawBacklight)
                + ", mRawNits=" + Arrays.toString(mRawNits)
@@ -336,9 +340,8 @@ public class DisplayDeviceConfig {
        final String filename = String.format(CONFIG_FILE_FORMAT, suffix);
        final File filePath = Environment.buildPath(
                baseDirectory, ETC_DIR, DISPLAY_CONFIG_DIR, filename);
        if (filePath.exists()) {
        final DisplayDeviceConfig config = new DisplayDeviceConfig(context);
            config.initFromFile(filePath);
        if (config.initFromFile(filePath)) {
            return config;
        }
        return null;
@@ -356,15 +359,15 @@ public class DisplayDeviceConfig {
        return config;
    }

    private void initFromFile(File configFile) {
    private boolean initFromFile(File configFile) {
        if (!configFile.exists()) {
            // Display configuration files aren't required to exist.
            return;
            return false;
        }

        if (!configFile.isFile()) {
            Slog.e(TAG, "Display configuration is not a file: " + configFile + ", skipping");
            return;
            return false;
        }

        try (InputStream in = new BufferedInputStream(new FileInputStream(configFile))) {
@@ -385,6 +388,8 @@ public class DisplayDeviceConfig {
            Slog.e(TAG, "Encountered an error while reading/parsing display config file: "
                    + configFile, e);
        }
        mLoadedFrom = configFile.toString();
        return true;
    }

    private void initFromGlobalXml() {
@@ -395,10 +400,12 @@ public class DisplayDeviceConfig {
        loadBrightnessRampsFromConfigXml();
        loadAmbientLightSensorFromConfigXml();
        setProxSensorUnspecified();
        mLoadedFrom = "<config.xml>";
    }

    private void initFromDefaultValues() {
        // Set all to basic values
        mLoadedFrom = "Static values";
        mBacklightMinimum = PowerManager.BRIGHTNESS_MIN;
        mBacklightMaximum = PowerManager.BRIGHTNESS_MAX;
        mBrightnessDefault = BRIGHTNESS_DEFAULT;
@@ -689,6 +696,11 @@ public class DisplayDeviceConfig {
        if (sensorDetails != null) {
            mAmbientLightSensor.type = sensorDetails.getType();
            mAmbientLightSensor.name = sensorDetails.getName();
            final RefreshRateRange rr = sensorDetails.getRefreshRate();
            if (rr != null) {
                mAmbientLightSensor.minRefreshRate = rr.getMinimum().floatValue();
                mAmbientLightSensor.maxRefreshRate = rr.getMaximum().floatValue();
            }
        } else {
            loadAmbientLightSensorFromConfigXml();
        }
@@ -704,22 +716,42 @@ public class DisplayDeviceConfig {
        if (sensorDetails != null) {
            mProximitySensor.name = sensorDetails.getName();
            mProximitySensor.type = sensorDetails.getType();
            final RefreshRateRange rr = sensorDetails.getRefreshRate();
            if (rr != null) {
                mProximitySensor.minRefreshRate = rr.getMinimum().floatValue();
                mProximitySensor.maxRefreshRate = rr.getMaximum().floatValue();
            }
        } else {
            setProxSensorUnspecified();
        }
    }

    static class SensorIdentifier {
    static class SensorData {
        public String type;
        public String name;
        public float minRefreshRate = 0.0f;
        public float maxRefreshRate = Float.POSITIVE_INFINITY;

        @Override
        public String toString() {
            return "Sensor{"
                    + "type: \"" + type + "\""
                    + ", name: \"" + name + "\""
                    + "type: " + type
                    + ", name: " + name
                    + ", refreshRateRange: [" + minRefreshRate + ", " + maxRefreshRate + "]"
                    + "} ";
        }

        /**
         * @return True if the sensor matches both the specified name and type, or one if only
         * one is specified (not-empty). Always returns false if both parameters are null or empty.
         */
        public boolean matches(String sensorName, String sensorType) {
            final boolean isNameSpecified = !TextUtils.isEmpty(sensorName);
            final boolean isTypeSpecified = !TextUtils.isEmpty(sensorType);
            return (isNameSpecified || isTypeSpecified)
                    && (!isNameSpecified || sensorName.equals(name))
                    && (!isTypeSpecified || sensorType.equals(type));
        }
    }

    /**
+38 −0
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ import android.content.res.TypedArray;
import android.database.ContentObserver;
import android.graphics.ColorSpace;
import android.graphics.Point;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.display.AmbientBrightnessDayStats;
@@ -62,6 +63,7 @@ import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.display.DisplayManagerInternal.DisplayGroupListener;
import android.hardware.display.DisplayManagerInternal.DisplayTransactionListener;
import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
import android.hardware.display.DisplayViewport;
import android.hardware.display.DisplayedContentSample;
import android.hardware.display.DisplayedContentSamplingAttributes;
@@ -119,6 +121,8 @@ import com.android.server.DisplayThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.UiThread;
import com.android.server.display.DisplayDeviceConfig.SensorData;
import com.android.server.display.utils.SensorUtils;
import com.android.server.wm.SurfaceAnimationThread;
import com.android.server.wm.WindowManagerInternal;

@@ -3257,6 +3261,40 @@ public final class DisplayManagerService extends SystemService {
        public int getRefreshRateSwitchingType() {
            return getRefreshRateSwitchingTypeInternal();
        }

        @Override
        public RefreshRateRange getRefreshRateForDisplayAndSensor(int displayId, String sensorName,
                String sensorType) {
            final SensorManager sensorManager;
            synchronized (mSyncRoot) {
                sensorManager = mSensorManager;
            }
            if (sensorManager == null) {
                return null;
            }

            // Verify that the specified sensor exists.
            final Sensor sensor = SensorUtils.findSensor(sensorManager, sensorType, sensorName,
                    SensorUtils.NO_FALLBACK);
            if (sensor == null) {
                return null;
            }

            synchronized (mSyncRoot) {
                final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(displayId);
                final DisplayDevice device = display.getPrimaryDisplayDeviceLocked();
                if (device == null) {
                    return null;
                }
                final DisplayDeviceConfig config = device.getDisplayDeviceConfig();
                SensorData sensorData = config.getProximitySensor();
                if (sensorData.matches(sensorName, sensorType)) {
                    return new RefreshRateRange(sensorData.minRefreshRate,
                            sensorData.maxRefreshRate);
                }
            }
            return null;
        }
    }

    class DesiredDisplayModeSpecsObserver
+79 −63
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
import android.hardware.fingerprint.IUdfpsHbmListener;
import android.net.Uri;
import android.os.Handler;
@@ -51,6 +53,8 @@ import com.android.internal.os.BackgroundThread;
import com.android.server.LocalServices;
import com.android.server.display.utils.AmbientFilter;
import com.android.server.display.utils.AmbientFilterFactory;
import com.android.server.sensors.SensorManagerInternal;
import com.android.server.sensors.SensorManagerInternal.ProximityActiveListener;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.utils.DeviceConfigInterface;

@@ -85,8 +89,7 @@ public class DisplayModeDirector {

    private static final int INVALID_DISPLAY_MODE_ID = -1;

    // The tolerance within which we consider something approximately equals.
    private static final float FLOAT_TOLERANCE = 0.01f;
    private static final float FLOAT_TOLERANCE = RefreshRateRange.FLOAT_TOLERANCE;

    private final Object mLock = new Object();
    private final Context mContext;
@@ -98,6 +101,7 @@ public class DisplayModeDirector {
    private final SettingsObserver mSettingsObserver;
    private final DisplayObserver mDisplayObserver;
    private final UdfpsObserver mUdfpsObserver;
    private final SensorObserver mSensorObserver;
    private final DeviceConfigInterface mDeviceConfig;
    private final DeviceConfigDisplaySettings mDeviceConfigDisplaySettings;

@@ -139,6 +143,11 @@ public class DisplayModeDirector {
        mDisplayObserver = new DisplayObserver(context, handler);
        mBrightnessObserver = new BrightnessObserver(context, handler);
        mUdfpsObserver = new UdfpsObserver();
        mSensorObserver = new SensorObserver(context, (displayId, priority, vote) -> {
            synchronized (mLock) {
                updateVoteLocked(displayId, priority, vote);
            }
        });
        mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings();
        mDeviceConfig = injector.getDeviceConfig();
        mAlwaysRespectAppRequest = false;
@@ -155,6 +164,7 @@ public class DisplayModeDirector {
        mSettingsObserver.observe();
        mDisplayObserver.observe();
        mBrightnessObserver.observe(sensorManager);
        mSensorObserver.observe();
        synchronized (mLock) {
            // We may have a listener already registered before the call to start, so go ahead and
            // notify them to pick up our newly initialized state.
@@ -585,6 +595,7 @@ public class DisplayModeDirector {
            mAppRequestObserver.dumpLocked(pw);
            mBrightnessObserver.dumpLocked(pw);
            mUdfpsObserver.dumpLocked(pw);
            mSensorObserver.dumpLocked(pw);
        }
    }

@@ -767,66 +778,6 @@ public class DisplayModeDirector {
        }
    }

    /**
     * Information about the min and max refresh rate DM would like to set the display to.
     */
    public static final class RefreshRateRange {
        /**
         * The lowest desired refresh rate.
         */
        public float min;
        /**
         * The highest desired refresh rate.
         */
        public float max;

        public RefreshRateRange() {}

        public RefreshRateRange(float min, float max) {
            if (min < 0 || max < 0 || min > max + FLOAT_TOLERANCE) {
                Slog.e(TAG, "Wrong values for min and max when initializing RefreshRateRange : "
                        + min + " " + max);
                this.min = this.max = 0;
                return;
            }
            if (min > max) {
                // Min and max are within epsilon of each other, but in the wrong order.
                float t = min;
                min = max;
                max = t;
            }
            this.min = min;
            this.max = max;
        }

        /**
         * Checks whether the two objects have the same values.
         */
        @Override
        public boolean equals(Object other) {
            if (other == this) {
                return true;
            }

            if (!(other instanceof RefreshRateRange)) {
                return false;
            }

            RefreshRateRange refreshRateRange = (RefreshRateRange) other;
            return (min == refreshRateRange.min && max == refreshRateRange.max);
        }

        @Override
        public int hashCode() {
            return Objects.hash(min, max);
        }

        @Override
        public String toString() {
            return "(" + min + " " + max + ")";
        }
    }

    /**
     * Information about the desired display mode to be set by the system. Includes the base
     * mode ID and the primary and app request refresh rate ranges.
@@ -987,9 +938,13 @@ public class DisplayModeDirector {
        // user seeing the display flickering when the switches occur.
        public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 8;

        // The proximity sensor needs the refresh rate to be locked in order to function, so this is
        // set to a high priority.
        public static final int PRIORITY_PROXIMITY = 9;

        // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
        // to function, so this needs to be the highest priority of all votes.
        public static final int PRIORITY_UDFPS = 9;
        public static final int PRIORITY_UDFPS = 10;

        // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
        // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
@@ -1086,6 +1041,8 @@ public class DisplayModeDirector {
                    return "PRIORITY_LOW_POWER_MODE";
                case PRIORITY_UDFPS:
                    return "PRIORITY_UDFPS";
                case PRIORITY_PROXIMITY:
                    return "PRIORITY_PROXIMITY";

                default:
                    return Integer.toString(priority);
@@ -2142,6 +2099,62 @@ public class DisplayModeDirector {
        }
    }

    private static class SensorObserver implements ProximityActiveListener {
        private static final String PROXIMITY_SENSOR_NAME = null;
        private static final String PROXIMITY_SENSOR_TYPE = Sensor.STRING_TYPE_PROXIMITY;

        private final BallotBox mBallotBox;
        private final Context mContext;

        private DisplayManager mDisplayManager;
        private DisplayManagerInternal mDisplayManagerInternal;
        private boolean mIsProxActive = false;

        SensorObserver(Context context, BallotBox ballotBox) {
            mContext = context;
            mBallotBox = ballotBox;
        }

        @Override
        public void onProximityActive(boolean isActive) {
            if (mIsProxActive != isActive) {
                mIsProxActive = isActive;
                recalculateVotes();
            }
        }

        public void observe() {
            mDisplayManager = mContext.getSystemService(DisplayManager.class);
            mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);

            final SensorManagerInternal sensorManager =
                    LocalServices.getService(SensorManagerInternal.class);
            sensorManager.addProximityActiveListener(BackgroundThread.getExecutor(), this);
        }

        private void recalculateVotes() {
            final Display[] displays = mDisplayManager.getDisplays();
            for (Display d : displays) {
                int displayId = d.getDisplayId();
                Vote vote = null;
                if (mIsProxActive) {
                    final RefreshRateRange rate =
                            mDisplayManagerInternal.getRefreshRateForDisplayAndSensor(
                                    displayId, PROXIMITY_SENSOR_NAME, PROXIMITY_SENSOR_TYPE);
                    if (rate != null) {
                        vote = Vote.forRefreshRates(rate.min, rate.max);
                    }
                }
                mBallotBox.vote(displayId, Vote.PRIORITY_PROXIMITY, vote);
            }
        }

        void dumpLocked(PrintWriter pw) {
            pw.println("  SensorObserver");
            pw.println("    mIsProxActive=" + mIsProxActive);
        }
    }

    private class DeviceConfigDisplaySettings implements DeviceConfig.OnPropertiesChangedListener {
        public DeviceConfigDisplaySettings() {
        }
@@ -2328,4 +2341,7 @@ public class DisplayModeDirector {
        }
    }

    interface BallotBox {
        void vote(int displayId, int priority, Vote vote);
    }
}
+11 −33
Original line number Diff line number Diff line
@@ -48,7 +48,6 @@ import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.util.MathUtils;
import android.util.Slog;
@@ -65,13 +64,13 @@ import com.android.server.am.BatteryStatsService;
import com.android.server.display.RampAnimator.DualRampAnimator;
import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener;
import com.android.server.display.utils.SensorUtils;
import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
import com.android.server.display.whitebalance.DisplayWhiteBalanceFactory;
import com.android.server.display.whitebalance.DisplayWhiteBalanceSettings;
import com.android.server.policy.WindowManagerPolicy;

import java.io.PrintWriter;
import java.util.List;

/**
 * Controls the power state of the display.
@@ -586,26 +585,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
        mBrightnessMapper.recalculateSplines(mCdsi.isReduceBrightColorsActivated(), adjustedNits);
    }

    private Sensor findSensor(String sensorType, String sensorName, int fallbackType,
            boolean useFallback) {
        final boolean isNameSpecified = !TextUtils.isEmpty(sensorName);
        final boolean isTypeSpecified = !TextUtils.isEmpty(sensorType);
        List<Sensor> sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
        if (isNameSpecified || isTypeSpecified) {
            for (Sensor sensor : sensors) {
                if ((!isNameSpecified || sensorName.equals(sensor.getName()))
                        && (!isTypeSpecified || sensorType.equals(sensor.getStringType()))) {
                    return sensor;
                }
            }
        }
        if (useFallback) {
            return mSensorManager.getDefaultSensor(fallbackType);
        } else {
            return null;
        }
    }

    /**
     * Returns true if the proximity sensor screen-off function is available.
     */
@@ -1654,24 +1633,23 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
    }

    private void loadAmbientLightSensor() {
        DisplayDeviceConfig.SensorIdentifier lightSensor =
                mDisplayDeviceConfig.getAmbientLightSensor();
        String lightSensorName = lightSensor.name;
        String lightSensorType = lightSensor.type;
        mLightSensor = findSensor(lightSensorType, lightSensorName, Sensor.TYPE_LIGHT,
                mDisplayId == Display.DEFAULT_DISPLAY);
        DisplayDeviceConfig.SensorData lightSensor = mDisplayDeviceConfig.getAmbientLightSensor();
        final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY
                ? Sensor.TYPE_LIGHT : SensorUtils.NO_FALLBACK;
        mLightSensor = SensorUtils.findSensor(mSensorManager, lightSensor.type, lightSensor.name,
                fallbackType);
    }

    private void loadProximitySensor() {
        if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT) {
            return;
        }
        final DisplayDeviceConfig.SensorIdentifier proxSensor =
        final DisplayDeviceConfig.SensorData proxSensor =
                mDisplayDeviceConfig.getProximitySensor();
        final String proxSensorName = proxSensor.name;
        final String proxSensorType = proxSensor.type;
        mProximitySensor = findSensor(proxSensorType, proxSensorName, Sensor.TYPE_PROXIMITY,
                mDisplayId == Display.DEFAULT_DISPLAY);
        final int fallbackType = mDisplayId == Display.DEFAULT_DISPLAY
                ? Sensor.TYPE_PROXIMITY : SensorUtils.NO_FALLBACK;
        mProximitySensor = SensorUtils.findSensor(mSensorManager, proxSensor.type, proxSensor.name,
                fallbackType);
        if (mProximitySensor != null) {
            mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(),
                    TYPICAL_PROXIMITY_THRESHOLD);
Loading