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

Commit ea2e67a5 authored by Miranda Kephart's avatar Miranda Kephart
Browse files

Add corner flow for screenshots

Instead of showing a notification, the screenshot animates into the
bottom left corner of the phone. Tapping on it leads to the same
intent as tapping "Edit" on the notification would. The screenshot
disappears after 8 seconds.

Bug: 137153302
Test: adds feature behind flag, tested that behavior remains the same
if flag is false

Change-Id: I4f48ba7de36f298bd6a66439ea6885fc92a8cb9c
parent bd1b494b
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -337,6 +337,14 @@ public final class SystemUiDeviceConfigFlags {
            "brightline_falsing_zigzag_y_secondary_deviance";


    // Flags related to screenshots

    /**
     * (boolean) Whether screenshot flow going to the corner (instead of shown in a notification)
     * is enabled.
     */
    public static final String SCREENSHOT_CORNER_FLOW = "screenshot_corner_flow";

    private SystemUiDeviceConfigFlags() {
    }
}
+166 −46
Original line number Diff line number Diff line
@@ -17,8 +17,10 @@
package com.android.systemui.screenshot;

import static android.content.Context.NOTIFICATION_SERVICE;
import static android.provider.DeviceConfig.NAMESPACE_SYSTEMUI;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;

import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_CORNER_FLOW;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_CANCEL_NOTIFICATION;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP;
@@ -29,6 +31,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.Notification;
import android.app.Notification.BigPictureStyle;
@@ -59,12 +62,17 @@ import android.media.MediaActionSound;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.Process;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Slog;
import android.view.Display;
import android.view.LayoutInflater;
@@ -96,6 +104,7 @@ import java.util.Date;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;

import javax.inject.Inject;
import javax.inject.Singleton;
@@ -109,6 +118,7 @@ class SaveImageInBackgroundData {
    Bitmap image;
    Uri imageUri;
    Runnable finisher;
    Function<PendingIntent, Void> onEditReady;
    int iconSize;
    int previewWidth;
    int previewheight;
@@ -343,6 +353,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
                    R.drawable.ic_screenshot_edit,
                    r.getString(com.android.internal.R.string.screenshot_edit), editAction);
            mNotificationBuilder.addAction(editActionBuilder.build());
            if (editAction != null && mParams.onEditReady != null) {
                mParams.onEditReady.apply(editAction);
            }

            // Create a delete action for the notification
            PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode,
@@ -379,6 +392,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
            // Show a message that we've failed to save the image to disk
            GlobalScreenshot.notifyScreenshotError(mParams.context, mNotificationManager,
                    mParams.errorMsgResId);
        } else {
            if (mParams.onEditReady != null) {
                // Cancel the "saving screenshot" notification
                mNotificationManager.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);
            } else {
                // Show the final notification to indicate screenshot saved
                Context context = mParams.context;
@@ -396,7 +413,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
                mPublicNotificationBuilder
                        .setContentTitle(r.getString(R.string.screenshot_saved_title))
                        .setContentText(r.getString(R.string.screenshot_saved_text))
                    .setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
                        .setContentIntent(
                                PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
                        .setWhen(now)
                        .setAutoCancel(true)
                        .setColor(context.getColor(
@@ -415,6 +433,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
                mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT,
                        mNotificationBuilder.build());
            }
        }
        mParams.finisher.run();
        mParams.clearContext();
    }
@@ -473,8 +492,12 @@ public class GlobalScreenshot {
    private static final float SCREENSHOT_SCALE = 1f;
    private static final float SCREENSHOT_DROP_IN_MIN_SCALE = SCREENSHOT_SCALE * 0.725f;
    private static final float SCREENSHOT_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.45f;
    private static final float SCREENSHOT_CORNER_MIN_SCALE = SCREENSHOT_SCALE * 0.2f;
    private static final float SCREENSHOT_FAST_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.6f;
    private static final float SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET = 0f;
    private static final float SCREENSHOT_CORNER_MIN_SCALE_OFFSET = .1f;
    private static final long SCREENSHOT_CORNER_TIMEOUT_MILLIS = 8000;
    private static final int MESSAGE_CORNER_TIMEOUT = 2;
    private final int mPreviewWidth;
    private final int mPreviewHeight;

@@ -502,6 +525,19 @@ public class GlobalScreenshot {

    private MediaActionSound mCameraSound;

    private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_CORNER_TIMEOUT:
                    GlobalScreenshot.this.clearScreenshot();
                    break;
                default:
                    break;
            }
        }
    };


    /**
     * @param context everything needs a context :(
@@ -574,12 +610,14 @@ public class GlobalScreenshot {
    /**
     * Creates a new worker thread and saves the screenshot to the media store.
     */
    private void saveScreenshotInWorkerThread(Runnable finisher) {
    private void saveScreenshotInWorkerThread(
            Runnable finisher, @Nullable Function<PendingIntent, Void> onEditReady) {
        SaveImageInBackgroundData data = new SaveImageInBackgroundData();
        data.context = mContext;
        data.image = mScreenBitmap;
        data.iconSize = mNotificationIconSize;
        data.finisher = finisher;
        data.onEditReady = onEditReady;
        data.previewWidth = mPreviewWidth;
        data.previewheight = mPreviewHeight;
        if (mSaveInBgTask != null) {
@@ -589,6 +627,10 @@ public class GlobalScreenshot {
                .execute();
    }

    private void saveScreenshotInWorkerThread(Runnable finisher) {
        saveScreenshotInWorkerThread(finisher, null);
    }

    /**
     * Takes a screenshot of the current display and shows an animation.
     */
@@ -682,6 +724,22 @@ public class GlobalScreenshot {
        }
    }

    /**
     * Clears current screenshot
     */
    private void clearScreenshot() {
        if (mScreenshotLayout.isAttachedToWindow()) {
            mWindowManager.removeView(mScreenshotLayout);
        }

        // Clear any references to the bitmap
        mScreenBitmap = null;
        mScreenshotView.setImageBitmap(null);
        mBackgroundView.setVisibility(View.GONE);
        mScreenshotView.setVisibility(View.GONE);
        mScreenshotView.setLayerType(View.LAYER_TYPE_NONE, null);
    }

    /**
     * Starts the animation after taking the screenshot
     */
@@ -706,34 +764,55 @@ public class GlobalScreenshot {
            mScreenshotAnimation.removeAllListeners();
        }

        boolean useCornerFlow =
                DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, SCREENSHOT_CORNER_FLOW, false);
        mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
        ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation();
        ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h,
                statusBarVisible, navBarVisible);
        ValueAnimator screenshotFadeOutAnim = useCornerFlow
                ? createScreenshotToCornerAnimation(w, h)
                : createScreenshotDropOutAnimation(w, h, statusBarVisible, navBarVisible);
        mScreenshotAnimation = new AnimatorSet();
        mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim);
        mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // Save the screenshot once we have a bit of time now
                if (!useCornerFlow) {
                    saveScreenshotInWorkerThread(finisher);
                mWindowManager.removeView(mScreenshotLayout);

                // Clear any references to the bitmap
                mScreenBitmap = null;
                mScreenshotView.setImageBitmap(null);
                    clearScreenshot();
                } else {
                    mScreenshotView.requestFocus();
                    mScreenshotView.setOnClickListener((v) -> {
                        // TODO: remove once we have a better UI to show that we aren't ready yet
                        Toast notReadyToast = Toast.makeText(
                                mContext, "Screenshot is not ready yet", Toast.LENGTH_SHORT);
                        notReadyToast.show();
                    });
                    saveScreenshotInWorkerThread(finisher, intent -> {
                        mScreenshotHandler.post(() -> mScreenshotView.setOnClickListener(v -> {
                            try {
                                intent.send();
                                clearScreenshot();
                            } catch (PendingIntent.CanceledException e) {
                                Log.e(TAG, "Edit intent cancelled", e);
                            }
                            mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT);
                        }));
                        return null;
                    });
        mScreenshotLayout.post(new Runnable() {
            @Override
            public void run() {
                    mScreenshotHandler.sendMessageDelayed(
                            mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT),
                            SCREENSHOT_CORNER_TIMEOUT_MILLIS);
                }
            }
        });
        mScreenshotHandler.post(() -> {
            // Play the shutter sound to notify that we've taken a screenshot
            mCameraSound.play(MediaActionSound.SHUTTER_CLICK);

            mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
            mScreenshotView.buildLayer();
            mScreenshotAnimation.start();
            }
        });
    }
    private ValueAnimator createScreenshotDropInAnimation() {
@@ -878,6 +957,47 @@ public class GlobalScreenshot {
        return anim;
    }

    private ValueAnimator createScreenshotToCornerAnimation(int w, int h) {
        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
        anim.setStartDelay(SCREENSHOT_DROP_OUT_DELAY);

        final float scaleDurationPct =
                (float) SCREENSHOT_DROP_OUT_SCALE_DURATION / SCREENSHOT_DROP_OUT_DURATION;
        final Interpolator scaleInterpolator = new Interpolator() {
            @Override
            public float getInterpolation(float x) {
                if (x < scaleDurationPct) {
                    // Decelerate, and scale the input accordingly
                    return (float) (1f - Math.pow(1f - (x / scaleDurationPct), 2f));
                }
                return 1f;
            }
        };

        // Determine the bounds of how to scale
        float halfScreenWidth = (w - 2f * mBgPadding) / 2f;
        float halfScreenHeight = (h - 2f * mBgPadding) / 2f;
        final float offsetPct = SCREENSHOT_CORNER_MIN_SCALE_OFFSET;
        final PointF finalPos = new PointF(
                -halfScreenWidth + (SCREENSHOT_CORNER_MIN_SCALE + offsetPct) * halfScreenWidth,
                halfScreenHeight - (SCREENSHOT_CORNER_MIN_SCALE + offsetPct) * halfScreenHeight);

        // Animate the screenshot to the bottom left corner
        anim.setDuration(SCREENSHOT_DROP_OUT_DURATION);
        anim.addUpdateListener(animation -> {
            float t = (Float) animation.getAnimatedValue();
            float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale)
                    - scaleInterpolator.getInterpolation(t)
                    * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_CORNER_MIN_SCALE);
            mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA);
            mScreenshotView.setScaleX(scaleT);
            mScreenshotView.setScaleY(scaleT);
            mScreenshotView.setTranslationX(t * finalPos.x);
            mScreenshotView.setTranslationY(t * finalPos.y);
        });
        return anim;
    }

    static void notifyScreenshotError(Context context, NotificationManager nManager, int msgResId) {
        Resources r = context.getResources();
        String errorMsg = r.getString(msgResId);