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

Commit 92dfd673 authored by Jon Miranda's avatar Jon Miranda
Browse files

Screenshot to share sheet shared element transition.

Switching the share action to a Supplier interface allows us to
maintain the current code style, while ensuring that the
APIs required to start the transition are called when the
share chip is pressed.

Bug: 174811491
Test: manual

Change-Id: I201b82eaea650fcef23909212385fd6d56831429
parent 2acc16e9
Loading
Loading
Loading
Loading
+48 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.SharedElementCallback;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionManager;
import android.app.prediction.AppPredictor;
@@ -176,6 +177,13 @@ public class ChooserActivity extends ResolverActivity implements
    public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
            = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";

    /**
     * Transition name for the first image preview.
     * To be used for shared element transition into this activity.
     * @hide
     */
    public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "chooser_preview_image_1";

    private static final String PREF_NUM_SHEET_EXPANSIONS = "pref_num_sheet_expansions";

    private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label";
@@ -307,6 +315,8 @@ public class ChooserActivity extends ResolverActivity implements
    @VisibleForTesting
    protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;

    private boolean mRemoveSharedElements = false;

    private class ContentPreviewCoordinator {
        private static final int IMAGE_FADE_IN_MILLIS = 150;
        private static final int IMAGE_LOAD_TIMEOUT = 1;
@@ -370,10 +380,29 @@ public class ChooserActivity extends ResolverActivity implements
                        if (task.mExtraCount > 0) {
                            imageView.setExtraImageCount(task.mExtraCount);
                        }

                        setupPreDrawForSharedElementTransition(imageView);
                }
            }
        };

        private void setupPreDrawForSharedElementTransition(View v) {
            v.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    v.getViewTreeObserver().removeOnPreDrawListener(this);

                    if (!mRemoveSharedElements && isActivityTransitionRunning()) {
                        // Disable the window animations as it interferes with the
                        // transition animation.
                        getWindow().setWindowAnimations(0);
                    }
                    startPostponedEnterTransition();
                    return true;
                }
            });
        }

        ContentPreviewCoordinator(View parentView, boolean hideParentOnFail) {
            super();

@@ -413,6 +442,8 @@ public class ChooserActivity extends ResolverActivity implements
                }
                mHideParentOnFail = false;
            }
            mRemoveSharedElements = true;
            startPostponedEnterTransition();
        }

        private void collapseParentView() {
@@ -794,6 +825,19 @@ public class ChooserActivity extends ResolverActivity implements
        );
        mDirectShareShortcutInfoCache = new HashMap<>();
        mChooserTargetComponentNameCache = new HashMap<>();

        setEnterSharedElementCallback(new SharedElementCallback() {
            @Override
            public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
                if (mRemoveSharedElements) {
                    names.remove(FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
                    sharedElements.remove(FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
                }
                super.onMapSharedElements(names, sharedElements);
                mRemoveSharedElements = false;
            }
        });
        postponeEnterTransition();
    }

    @Override
@@ -1337,6 +1381,8 @@ public class ChooserActivity extends ResolverActivity implements
        String action = targetIntent.getAction();
        if (Intent.ACTION_SEND.equals(action)) {
            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
            imagePreview.findViewById(R.id.content_preview_image_1_large)
                    .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
            mPreviewCoord.loadUriIntoView(R.id.content_preview_image_1_large, uri, 0);
        } else {
            ContentResolver resolver = getContentResolver();
@@ -1356,6 +1402,8 @@ public class ChooserActivity extends ResolverActivity implements
                return contentPreviewLayout;
            }

            imagePreview.findViewById(R.id.content_preview_image_1_large)
                    .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
            mPreviewCoord.loadUriIntoView(R.id.content_preview_image_1_large, imageUris.get(0), 0);

            if (imageUris.size() == 2) {
+69 −59
Original line number Diff line number Diff line
@@ -56,6 +56,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.systemui.R;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ShareTransition;

import java.io.File;
import java.io.IOException;
@@ -74,6 +75,7 @@ import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;

/**
 * An AsyncTask that saves an image to the media store in the background.
@@ -95,12 +97,15 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
    private final String mScreenshotId;
    private final boolean mSmartActionsEnabled;
    private final Random mRandom = new Random();
    private final Supplier<ShareTransition> mSharedElementTransition;

    SaveImageInBackgroundTask(Context context, ScreenshotSmartActions screenshotSmartActions,
            ScreenshotController.SaveImageInBackgroundData data) {
            ScreenshotController.SaveImageInBackgroundData data,
            Supplier<ShareTransition> sharedElementTransition) {
        mContext = context;
        mScreenshotSmartActions = screenshotSmartActions;
        mImageData = new ScreenshotController.SavedImageData();
        mSharedElementTransition = sharedElementTransition;

        // Prepare all the output metadata
        mParams = data;
@@ -135,7 +140,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {

        ContentResolver resolver = mContext.getContentResolver();
        Bitmap image = mParams.image;
        Resources r = mContext.getResources();

        try {
            // Save the screenshot to the MediaStore
@@ -233,7 +237,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {

            mImageData.uri = uri;
            mImageData.smartActions = smartActions;
            mImageData.shareAction = createShareAction(mContext, mContext.getResources(), uri);
            mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri);
            mImageData.editAction = createEditAction(mContext, mContext.getResources(), uri);
            mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri);

@@ -284,8 +288,14 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
        mParams.clearImage();
    }

    /**
     * Assumes that the action intent is sent immediately after being supplied.
     */
    @VisibleForTesting
    Notification.Action createShareAction(Context context, Resources r, Uri uri) {
    Supplier<ShareTransition> createShareAction(Context context, Resources r, Uri uri) {
        return () -> {
            ShareTransition transition = mSharedElementTransition.get();

            // Note: Both the share and edit actions are proxied through ActionProxyReceiver in
            // order to do some common work like dismissing the keyguard and sending
            // closeSystemWindows
@@ -310,8 +320,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
            // by setting the (otherwise unused) request code to the current user id.
            int requestCode = context.getUserId();

        Intent sharingChooserIntent =
                Intent.createChooser(sharingIntent, null)
            Intent sharingChooserIntent = Intent.createChooser(sharingIntent, null)
                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

@@ -319,7 +328,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
            PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
                    context, 0, sharingChooserIntent,
                    PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE,
                null, UserHandle.CURRENT);
                    transition.bundle, UserHandle.CURRENT);

            // Create a share action for the notification
            PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode,
@@ -338,7 +347,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
                    Icon.createWithResource(r, R.drawable.ic_screenshot_share),
                    r.getString(com.android.internal.R.string.share), shareAction);

        return shareActionBuilder.build();
            transition.shareAction = shareActionBuilder.build();
            return transition;
        };
    }

    @VisibleForTesting
@@ -354,8 +365,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
        if (!TextUtils.isEmpty(editorPackage)) {
            editIntent.setComponent(ComponentName.unflattenFromString(editorPackage));
        }
        editIntent.setType("image/png");
        editIntent.setData(uri);
        editIntent.setDataAndType(uri, "image/png");
        editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+50 −3
Original line number Diff line number Diff line
@@ -34,6 +34,9 @@ import static java.util.Objects.requireNonNull;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.ExitTransitionCoordinator;
import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks;
import android.app.Notification;
import android.content.ComponentName;
import android.content.Context;
@@ -46,6 +49,7 @@ import android.hardware.display.DisplayManager;
import android.media.MediaActionSound;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -54,6 +58,7 @@ import android.provider.DeviceConfig;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.view.Display;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -67,15 +72,18 @@ import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.Toast;

import com.android.internal.app.ChooserActivity;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.policy.PhoneWindow;
import com.android.settingslib.applications.InterestingConfigChanges;
import com.android.systemui.R;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ShareTransition;
import com.android.systemui.util.DeviceConfigProxy;

import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;

import javax.inject.Inject;

@@ -103,17 +111,26 @@ public class ScreenshotController {
     */
    static class SavedImageData {
        public Uri uri;
        public Notification.Action shareAction;
        public Supplier<ShareTransition> shareTransition;
        public Notification.Action editAction;
        public Notification.Action deleteAction;
        public List<Notification.Action> smartActions;

        /**
         * POD for shared element transition to share sheet.
         */
        static class ShareTransition {
            public Bundle bundle;
            public Notification.Action shareAction;
            public Runnable onCancelRunnable;
        }

        /**
         * Used to reset the return data on error
         */
        public void reset() {
            uri = null;
            shareAction = null;
            shareTransition = null;
            editAction = null;
            deleteAction = null;
            smartActions = null;
@@ -587,7 +604,8 @@ public class ScreenshotController {
            mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
        }

        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data);
        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data,
                getShareTransitionSupplier());
        mSaveInBgTask.execute();
    }

@@ -637,6 +655,35 @@ public class ScreenshotController {
        }
    }

    /**
     * Supplies the necessary bits for the shared element transition to share sheet.
     * Note that once supplied, the action intent to share must be sent immediately after.
     */
    private Supplier<ShareTransition> getShareTransitionSupplier() {
        return () -> {
            ExitTransitionCallbacks cb = new ExitTransitionCallbacks() {
                @Override
                public boolean isReturnTransitionAllowed() {
                    return false;
                }

                @Override
                public void onFinish() { }
            };

            Pair<ActivityOptions, ExitTransitionCoordinator> transition =
                    ActivityOptions.startSharedElementAnimation(mWindow, cb, null,
                            Pair.create(mScreenshotView.getScreenshotPreview(),
                                    ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME));
            transition.second.startExit();

            ShareTransition supply = new ShareTransition();
            supply.bundle = transition.first.toBundle();
            supply.onCancelRunnable = () -> ActivityOptions.stopSharedElementAnimation(mWindow);
            return supply;
        };
    }

    /**
     * Logs success/failure of the screenshot saving task, and shows an error if it failed.
     */
+28 −4
Original line number Diff line number Diff line
@@ -73,6 +73,7 @@ import android.widget.LinearLayout;

import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ShareTransition;
import com.android.systemui.shared.system.QuickStepContract;

import java.util.ArrayList;
@@ -104,6 +105,7 @@ public class ScreenshotView extends FrameLayout implements
    private static final long SCREENSHOT_DISMISS_Y_DURATION_MS = 350;
    private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 183;
    private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade
    private static final long SCREENSHOT_DISMISS_SHARE_OFFSET_MS = 300; // delay after share clicked
    private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
    private static final float ROUNDED_CORNER_RADIUS = .05f;
    private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
@@ -138,6 +140,7 @@ public class ScreenshotView extends FrameLayout implements
    private UiEventLogger mUiEventLogger;
    private ScreenshotViewCallback mCallbacks;
    private Animator mDismissAnimation;
    private boolean mIgnoreDismiss;

    private final ArrayList<ScreenshotActionChip> mSmartChips = new ArrayList<>();
    private PendingInteraction mPendingInteraction;
@@ -526,12 +529,30 @@ public class ScreenshotView extends FrameLayout implements
        });
        return animator;
    }
    protected View getScreenshotPreview() {
        return mScreenshotPreview;
    }

    void setChipIntents(ScreenshotController.SavedImageData imageData) {
        mShareChip.setPendingIntent(imageData.shareAction.actionIntent,
                () -> {
        mShareChip.setOnClickListener(v -> {
            ShareTransition transition = imageData.shareTransition.get();
            try {
                mIgnoreDismiss = true;
                transition.shareAction.actionIntent.send();
                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED);

                // Ensures that we delay dismissing until transition has started.
                postDelayed(() -> {
                    mIgnoreDismiss = false;
                    animateDismissal();
                }, SCREENSHOT_DISMISS_SHARE_OFFSET_MS);
            } catch (PendingIntent.CanceledException e) {
                mIgnoreDismiss = false;
                if (transition.onCancelRunnable != null) {
                    transition.onCancelRunnable.run();
                }
                Log.e(TAG, "Share intent cancelled", e);
            }
        });
        mEditChip.setPendingIntent(imageData.editAction.actionIntent,
                () -> {
@@ -589,6 +610,9 @@ public class ScreenshotView extends FrameLayout implements
    }

    private void animateDismissal(Animator dismissAnimation) {
        if (mIgnoreDismiss) {
            return;
        }
        if (DEBUG_WINDOW) {
            Log.d(TAG, "removing OnComputeInternalInsetsListener");
        }
+8 −4
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ import androidx.test.filters.SmallTest;

import com.android.systemui.SystemUIFactory;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ShareTransition;

import org.junit.Before;
import org.junit.Test;
@@ -175,10 +176,11 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
        data.finisher = null;
        data.mActionsReadyListener = null;
        SaveImageInBackgroundTask task =
                new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data);
                new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data,
                        ShareTransition::new);

        Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(),
                Uri.parse("Screenshot_123.png"));
                Uri.parse("Screenshot_123.png")).get().shareAction;

        Intent intent = shareAction.actionIntent.getIntent();
        assertNotNull(intent);
@@ -202,7 +204,8 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
        data.finisher = null;
        data.mActionsReadyListener = null;
        SaveImageInBackgroundTask task =
                new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data);
                new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data,
                        ShareTransition::new);

        Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(),
                Uri.parse("Screenshot_123.png"));
@@ -229,7 +232,8 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
        data.finisher = null;
        data.mActionsReadyListener = null;
        SaveImageInBackgroundTask task =
                new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data);
                new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data,
                        ShareTransition::new);

        Notification.Action deleteAction = task.createDeleteAction(mContext,
                mContext.getResources(),