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

Commit 33115b5f authored by Nicolo' Mazzucato's avatar Nicolo' Mazzucato
Browse files

Capture screenshots from all displays (flag-guarded)

This changes TakeScreenshotService to gather screenshots from all
(not-virtual) connected displays when a SystemUI flag is enabled.
System server part of the screenshot flow is unchanged.

When the `multi_display_screenshot` sysui flag is enabled, one
screenshot is captured for each display, and the UI is shown accordingly
in every display. This results in different files being saved.

When the flag is disabled, the previous behaviour is left unchanged.

ScreenshotController has been been slighly refactored to be per-display.
When the flag is disabled, only the controller for the default display
is created.

There is some minimal duplication of code related to UiEventLogger that
can be removed when the flag is enabled.

Test: TakeScreenshotServiceTest, TakeScreenshotExecutorTest, ConnectedDisplayInteractorTest, ScreenshotDataTest
Bug: 290910794
Bug: 295143676
Change-Id: I24bd6436d346c2cbd5dcdc201d01c8aabb09bc17
parent fa5fc1e3
Loading
Loading
Loading
Loading
+14 −10
Original line number Diff line number Diff line
@@ -28,8 +28,18 @@ import kotlinx.coroutines.launch
import java.util.function.Consumer
import javax.inject.Inject

/** Processes a screenshot request sent from [ScreenshotHelper]. */
interface ScreenshotRequestProcessor {
    /**
 * Processes a screenshot request sent from {@link ScreenshotHelper}.
     * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
     *
     * @param screenshot the screenshot to process
     */
    suspend fun process(screenshot: ScreenshotData): ScreenshotData
}

/**
 * Implementation of [ScreenshotRequestProcessor]
 */
@SysUISingleton
class RequestProcessor @Inject constructor(
@@ -38,7 +48,7 @@ class RequestProcessor @Inject constructor(
        private val flags: FeatureFlags,
        /** For the Java Async version, to invoke the callback. */
        @Application private val mainScope: CoroutineScope
) {
) : ScreenshotRequestProcessor {
    /**
     * Inspects the incoming request, returning a potentially modified request depending on policy.
     *
@@ -57,7 +67,6 @@ class RequestProcessor @Inject constructor(
        // regardless of the managed profile status.

        if (request.type != TAKE_SCREENSHOT_PROVIDED_IMAGE) {

            val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
            Log.d(TAG, "findPrimaryContent: $info")

@@ -99,12 +108,7 @@ class RequestProcessor @Inject constructor(
        }
    }

    /**
     * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
     *
     * @param screenshot the screenshot to process
     */
    suspend fun process(screenshot: ScreenshotData): ScreenshotData {
    override suspend fun process(screenshot: ScreenshotData): ScreenshotData {
        var result = screenshot

        // Apply work profile screenshots policy:
@@ -116,7 +120,7 @@ class RequestProcessor @Inject constructor(
        // regardless of the managed profile status.

        if (screenshot.type != TAKE_SCREENSHOT_PROVIDED_IMAGE) {
            val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
            val info = policy.findPrimaryContent(screenshot.displayId)
            Log.d(TAG, "findPrimaryContent: $info")
            result.taskId = info.taskId
            result.topComponent = info.component
+27 −18
Original line number Diff line number Diff line
@@ -100,11 +100,14 @@ import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.util.Assert;

import com.google.common.util.concurrent.ListenableFuture;

import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;

import java.io.File;
import java.util.List;
import java.util.concurrent.CancellationException;
@@ -118,7 +121,6 @@ import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Supplier;

import javax.inject.Inject;

/**
 * Controls the state and flow for screenshots.
@@ -275,7 +277,7 @@ public class ScreenshotController {
    private final ScrollCaptureClient mScrollCaptureClient;
    private final PhoneWindow mWindow;
    private final DisplayManager mDisplayManager;
    private final DisplayTracker mDisplayTracker;
    private final int mDisplayId;
    private final ScrollCaptureController mScrollCaptureController;
    private final LongScreenshotData mLongScreenshotHolder;
    private final boolean mIsLowRamDevice;
@@ -314,7 +316,8 @@ public class ScreenshotController {
                    | ActivityInfo.CONFIG_SCREEN_LAYOUT
                    | ActivityInfo.CONFIG_ASSETS_PATHS);

    @Inject

    @AssistedInject
    ScreenshotController(
            Context context,
            FeatureFlags flags,
@@ -335,7 +338,7 @@ public class ScreenshotController {
            UserManager userManager,
            AssistContentRequester assistContentRequester,
            MessageContainerController messageContainerController,
            DisplayTracker displayTracker
            @Assisted int displayId
    ) {
        mScreenshotSmartActions = screenshotSmartActions;
        mNotificationsController = screenshotNotificationsController;
@@ -360,9 +363,9 @@ public class ScreenshotController {
            dismissScreenshot(SCREENSHOT_INTERACTION_TIMEOUT);
        });

        mDisplayId = displayId;
        mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
        mDisplayTracker = displayTracker;
        final Context displayContext = context.createDisplayContext(getDefaultDisplay());
        final Context displayContext = context.createDisplayContext(getDisplay());
        mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
        mWindowManager = mContext.getSystemService(WindowManager.class);
        mFlags = flags;
@@ -406,7 +409,7 @@ public class ScreenshotController {
        if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) {
            Rect bounds = getFullScreenRect();
            screenshot.setBitmap(
                    mImageCapture.captureDisplay(mDisplayTracker.getDefaultDisplayId(), bounds));
                    mImageCapture.captureDisplay(mDisplayId, bounds));
            screenshot.setScreenBounds(bounds);
        }

@@ -638,7 +641,7 @@ public class ScreenshotController {
                setWindowFocusable(false);
            }
        }, mActionExecutor, mFlags);
        mScreenshotView.setDefaultDisplay(mDisplayTracker.getDefaultDisplayId());
        mScreenshotView.setDefaultDisplay(mDisplayId);
        mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());

        mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
@@ -727,8 +730,8 @@ public class ScreenshotController {
        if (mLastScrollCaptureRequest != null) {
            mLastScrollCaptureRequest.cancel(true);
        }
        final ListenableFuture<ScrollCaptureResponse> future =
                mScrollCaptureClient.request(mDisplayTracker.getDefaultDisplayId());
        final ListenableFuture<ScrollCaptureResponse> future = mScrollCaptureClient.request(
                mDisplayId);
        mLastScrollCaptureRequest = future;
        mLastScrollCaptureRequest.addListener(() ->
                onScrollCaptureResponseReady(future, owner), mMainExecutor);
@@ -758,9 +761,8 @@ public class ScreenshotController {
            final ScrollCaptureResponse response = mLastScrollCaptureResponse;
            mScreenshotView.showScrollChip(response.getPackageName(), /* onClick */ () -> {
                DisplayMetrics displayMetrics = new DisplayMetrics();
                getDefaultDisplay().getRealMetrics(displayMetrics);
                Bitmap newScreenshot = mImageCapture.captureDisplay(
                        mDisplayTracker.getDefaultDisplayId(),
                getDisplay().getRealMetrics(displayMetrics);
                Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId,
                        new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));

                mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
@@ -825,7 +827,7 @@ public class ScreenshotController {
            try {
                WindowManagerGlobal.getWindowManagerService()
                        .overridePendingAppTransitionRemote(runner,
                                mDisplayTracker.getDefaultDisplayId());
                                mDisplayId);
            } catch (Exception e) {
                Log.e(TAG, "Error overriding screenshot app transition", e);
            }
@@ -1160,8 +1162,8 @@ public class ScreenshotController {
        }
    }

    private Display getDefaultDisplay() {
        return mDisplayManager.getDisplay(mDisplayTracker.getDefaultDisplayId());
    private Display getDisplay() {
        return mDisplayManager.getDisplay(mDisplayId);
    }

    private boolean allowLongScreenshots() {
@@ -1170,7 +1172,7 @@ public class ScreenshotController {

    private Rect getFullScreenRect() {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        getDefaultDisplay().getRealMetrics(displayMetrics);
        getDisplay().getRealMetrics(displayMetrics);
        return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
    }

@@ -1229,4 +1231,11 @@ public class ScreenshotController {
            };
        }
    }

    /** Injectable factory to create screenshot controller instances for a specific display. */
    @AssistedFactory
    public interface Factory {
        /** Creates an instance of the controller for that specific displayId. */
        ScreenshotController create(int displayId);
    }
}
+26 −15
Original line number Diff line number Diff line
@@ -6,12 +6,13 @@ import android.graphics.Insets
import android.graphics.Rect
import android.net.Uri
import android.os.UserHandle
import android.view.Display
import android.view.WindowManager.ScreenshotSource
import android.view.WindowManager.ScreenshotType
import androidx.annotation.VisibleForTesting
import com.android.internal.util.ScreenshotRequest

/** ScreenshotData represents the current state of a single screenshot being acquired. */
/** [ScreenshotData] represents the current state of a single screenshot being acquired. */
data class ScreenshotData(
    @ScreenshotType var type: Int,
    @ScreenshotSource var source: Int,
@@ -23,6 +24,7 @@ data class ScreenshotData(
    var taskId: Int,
    var insets: Insets,
    var bitmap: Bitmap?,
    var displayId: Int,
    /** App-provided URL representing the content the user was looking at in the screenshot. */
    var contextUrl: Uri? = null,
) {
@@ -31,22 +33,31 @@ data class ScreenshotData(

    companion object {
        @JvmStatic
        fun fromRequest(request: ScreenshotRequest): ScreenshotData {
            return ScreenshotData(
                request.type,
                request.source,
                if (request.userId >= 0) UserHandle.of(request.userId) else null,
                request.topComponent,
                request.boundsInScreen,
                request.taskId,
                request.insets,
                request.bitmap,
        fun fromRequest(request: ScreenshotRequest, displayId: Int = Display.DEFAULT_DISPLAY) =
            ScreenshotData(
                type = request.type,
                source = request.source,
                userHandle = if (request.userId >= 0) UserHandle.of(request.userId) else null,
                topComponent = request.topComponent,
                screenBounds = request.boundsInScreen,
                taskId = request.taskId,
                insets = request.insets,
                bitmap = request.bitmap,
                displayId = displayId,
            )
        }

        @VisibleForTesting
        fun forTesting(): ScreenshotData {
            return ScreenshotData(0, 0, null, null, null, 0, Insets.NONE, null)
        }
        fun forTesting() =
            ScreenshotData(
                type = 0,
                source = 0,
                userHandle = null,
                topComponent = null,
                screenBounds = null,
                taskId = 0,
                insets = Insets.NONE,
                bitmap = null,
                displayId = Display.DEFAULT_DISPLAY,
            )
    }
}
+201 −0
Original line number Diff line number Diff line
package com.android.systemui.screenshot

import android.net.Uri
import android.os.Trace
import android.util.Log
import android.view.Display
import com.android.internal.logging.UiEventLogger
import com.android.internal.util.ScreenshotRequest
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
import java.util.function.Consumer
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/**
 * Receives the signal to take a screenshot from [TakeScreenshotService], and calls back with the
 * result.
 *
 * Captures a screenshot for each [Display] available.
 */
@SysUISingleton
class TakeScreenshotExecutor
@Inject
constructor(
    private val screenshotControllerFactory: ScreenshotController.Factory,
    displayRepository: DisplayRepository,
    @Application private val mainScope: CoroutineScope,
    private val screenshotRequestProcessor: ScreenshotRequestProcessor,
    private val uiEventLogger: UiEventLogger
) {

    private lateinit var displays: StateFlow<Set<Display>>
    private val displaysCollectionJob: Job =
        mainScope.launch {
            displays = displayRepository.displays.stateIn(this, SharingStarted.Eagerly, emptySet())
        }

    private val screenshotControllers = mutableMapOf<Int, ScreenshotController>()

    /**
     * Executes the [ScreenshotRequest].
     *
     * [onSaved] is invoked only on the default display result. [RequestCallback.onFinish] is
     * invoked only when both screenshot UIs are removed.
     */
    suspend fun executeScreenshots(
        screenshotRequest: ScreenshotRequest,
        onSaved: (Uri) -> Unit,
        requestCallback: RequestCallback
    ) {
        val displayIds = getDisplaysToScreenshot()
        val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
        screenshotRequest.oneForEachDisplay(displayIds).forEach { screenshotData: ScreenshotData ->
            dispatchToController(
                screenshotData = screenshotData,
                onSaved =
                    if (screenshotData.displayId == Display.DEFAULT_DISPLAY) onSaved else { _ -> },
                callback = resultCallbackWrapper.createCallbackForId(screenshotData.displayId)
            )
        }
    }

    /** Creates a [ScreenshotData] for each display. */
    private suspend fun ScreenshotRequest.oneForEachDisplay(
        displayIds: List<Int>
    ): List<ScreenshotData> {
        return displayIds
            .map { displayId -> ScreenshotData.fromRequest(this, displayId) }
            .map { screenshotData: ScreenshotData ->
                screenshotRequestProcessor.process(screenshotData)
            }
    }

    private fun dispatchToController(
        screenshotData: ScreenshotData,
        onSaved: (Uri) -> Unit,
        callback: RequestCallback
    ) {
        uiEventLogger.log(
            ScreenshotEvent.getScreenshotSource(screenshotData.source),
            0,
            screenshotData.packageNameString
        )
        Log.d(TAG, "Screenshot request: $screenshotData")
        getScreenshotController(screenshotData.displayId)
            .handleScreenshot(screenshotData, onSaved, callback)
    }

    private fun getDisplaysToScreenshot(): List<Int> {
        return displays.value.filter { it.type in ALLOWED_DISPLAY_TYPES }.map { it.displayId }
    }

    /**
     * Propagates the close system dialog signal to all controllers.
     *
     * TODO(b/295143676): Move the receiver in this class once the flag is flipped.
     */
    fun onCloseSystemDialogsReceived() {
        screenshotControllers.forEach { (_, screenshotController) ->
            if (!screenshotController.isPendingSharedTransition) {
                screenshotController.dismissScreenshot(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER)
            }
        }
    }

    /** Removes all screenshot related windows. */
    fun removeWindows() {
        screenshotControllers.forEach { (_, screenshotController) ->
            screenshotController.removeWindow()
        }
    }

    /**
     * Destroys the executor. Afterwards, this class is not expected to work as intended anymore.
     */
    fun onDestroy() {
        screenshotControllers.forEach { (_, screenshotController) ->
            screenshotController.onDestroy()
        }
        screenshotControllers.clear()
        displaysCollectionJob.cancel()
    }

    private fun getScreenshotController(id: Int): ScreenshotController {
        return screenshotControllers.computeIfAbsent(id) { screenshotControllerFactory.create(id) }
    }

    /** For java compatibility only. see [executeScreenshots] */
    fun executeScreenshotsAsync(
        screenshotRequest: ScreenshotRequest,
        onSaved: Consumer<Uri>,
        requestCallback: RequestCallback
    ) {
        mainScope.launch {
            executeScreenshots(screenshotRequest, { uri -> onSaved.accept(uri) }, requestCallback)
        }
    }

    /**
     * Returns a [RequestCallback] that calls [RequestCallback.onFinish] only when all callbacks for
     * id created have finished.
     *
     * If any callback created calls [reportError], then following [onFinish] are not considered.
     */
    private class MultiResultCallbackWrapper(
        private val originalCallback: RequestCallback,
    ) {
        private val idsPending = mutableSetOf<Int>()
        private var errorReported = false

        /**
         * Creates a callback for [id].
         *
         * [originalCallback]'s [onFinish] will be called only when this (and the other created)
         * callback's [onFinish] have been called.
         */
        fun createCallbackForId(id: Int): RequestCallback {
            Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TAG, "Waiting for id=$id", id)
            idsPending += id
            return object : RequestCallback {
                override fun reportError() {
                    Log.d(TAG, "ReportError id=$id")
                    Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TAG, id)
                    Trace.instantForTrack(Trace.TRACE_TAG_APP, TAG, "reportError id=$id")
                    originalCallback.reportError()
                    errorReported = true
                }

                override fun onFinish() {
                    Log.d(TAG, "onFinish id=$id")
                    if (errorReported) return
                    idsPending -= id
                    Trace.instantForTrack(Trace.TRACE_TAG_APP, TAG, "onFinish id=$id")
                    if (idsPending.isEmpty()) {
                        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TAG, id)
                        originalCallback.onFinish()
                    }
                }
            }
        }
    }

    private companion object {
        val TAG = LogConfig.logTag(TakeScreenshotService::class.java)

        val ALLOWED_DISPLAY_TYPES =
            listOf(
                Display.TYPE_EXTERNAL,
                Display.TYPE_INTERNAL,
                Display.TYPE_OVERLAY,
                Display.TYPE_WIFI
            )
    }
}
+40 −12
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;

import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE;
import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_URI;
import static com.android.systemui.flags.Flags.MULTI_DISPLAY_SCREENSHOT;
import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
import static com.android.systemui.screenshot.LogConfig.DEBUG_SERVICE;
@@ -46,6 +47,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import android.view.Display;
import android.widget.Toast;

import com.android.internal.annotations.VisibleForTesting;
@@ -59,6 +61,7 @@ import java.util.concurrent.Executor;
import java.util.function.Consumer;

import javax.inject.Inject;
import javax.inject.Provider;

public class TakeScreenshotService extends Service {
    private static final String TAG = logTag(TakeScreenshotService.class);
@@ -82,12 +85,17 @@ public class TakeScreenshotService extends Service {
                if (DEBUG_DISMISS) {
                    Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS");
                }
                if (!mScreenshot.isPendingSharedTransition()) {
                if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
                    // TODO(b/295143676): move receiver inside executor when the flag is enabled.
                    mTakeScreenshotExecutor.get().onCloseSystemDialogsReceived();
                } else if (!mScreenshot.isPendingSharedTransition()) {
                    mScreenshot.dismissScreenshot(SCREENSHOT_DISMISSED_OTHER);
                }
            }
        }
    };
    private final Provider<TakeScreenshotExecutor> mTakeScreenshotExecutor;


    /** Informs about coarse grained state of the Controller. */
    public interface RequestCallback {
@@ -99,16 +107,15 @@ public class TakeScreenshotService extends Service {
    }

    @Inject
    public TakeScreenshotService(ScreenshotController screenshotController, UserManager userManager,
            DevicePolicyManager devicePolicyManager, UiEventLogger uiEventLogger,
            ScreenshotNotificationsController notificationsController, Context context,
            @Background Executor bgExecutor, FeatureFlags featureFlags,
            RequestProcessor processor) {
    public TakeScreenshotService(ScreenshotController.Factory screenshotControllerFactory,
            UserManager userManager, DevicePolicyManager devicePolicyManager,
            UiEventLogger uiEventLogger, ScreenshotNotificationsController notificationsController,
            Context context, @Background Executor bgExecutor, FeatureFlags featureFlags,
            RequestProcessor processor, Provider<TakeScreenshotExecutor> takeScreenshotExecutor) {
        if (DEBUG_SERVICE) {
            Log.d(TAG, "new " + this);
        }
        mHandler = new Handler(Looper.getMainLooper(), this::handleMessage);
        mScreenshot = screenshotController;
        mUserManager = userManager;
        mDevicePolicyManager = devicePolicyManager;
        mUiEventLogger = uiEventLogger;
@@ -117,6 +124,12 @@ public class TakeScreenshotService extends Service {
        mBgExecutor = bgExecutor;
        mFeatureFlags = featureFlags;
        mProcessor = processor;
        mTakeScreenshotExecutor = takeScreenshotExecutor;
        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
            mScreenshot = null;
        } else {
            mScreenshot = screenshotControllerFactory.create(Display.DEFAULT_DISPLAY);
        }
    }

    @Override
@@ -142,7 +155,11 @@ public class TakeScreenshotService extends Service {
        if (DEBUG_SERVICE) {
            Log.d(TAG, "onUnbind");
        }
        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
            mTakeScreenshotExecutor.get().removeWindows();
        } else {
            mScreenshot.removeWindow();
        }
        unregisterReceiver(mCloseSystemDialogs);
        return false;
    }
@@ -150,7 +167,11 @@ public class TakeScreenshotService extends Service {
    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
            mTakeScreenshotExecutor.get().onDestroy();
        } else {
            mScreenshot.onDestroy();
        }
        if (DEBUG_SERVICE) {
            Log.d(TAG, "onDestroy");
        }
@@ -218,10 +239,17 @@ public class TakeScreenshotService extends Service {
        }

        Log.d(TAG, "Processing screenshot data");
        ScreenshotData screenshotData = ScreenshotData.fromRequest(request);


        ScreenshotData screenshotData = ScreenshotData.fromRequest(
                request, Display.DEFAULT_DISPLAY);
        try {
            mProcessor.processAsync(screenshotData,
                    (data) -> dispatchToController(data, onSaved, callback));
            if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
                mTakeScreenshotExecutor.get().executeScreenshotsAsync(request, onSaved, callback);
            } else {
                mProcessor.processAsync(screenshotData, (data) ->
                        dispatchToController(data, onSaved, callback));
            }
        } catch (IllegalStateException e) {
            Log.e(TAG, "Failed to process screenshot request!", e);
            logFailedRequest(request);
Loading