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
This commit is contained in:
Jon Miranda
2020-12-16 01:33:34 +00:00
parent 2acc16e937
commit 92dfd67351
5 changed files with 196 additions and 63 deletions

View File

@@ -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) {

View File

@@ -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,61 +288,68 @@ 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) {
// 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
Supplier<ShareTransition> createShareAction(Context context, Resources r, Uri uri) {
return () -> {
ShareTransition transition = mSharedElementTransition.get();
// Create a share intent, this will always go through the chooser activity first
// which should not trigger auto-enter PiP
String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
Intent sharingIntent = new Intent(Intent.ACTION_SEND);
sharingIntent.setType("image/png");
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
// Include URI in ClipData also, so that grantPermission picks it up.
// We don't use setData here because some apps interpret this as "to:".
ClipData clipdata = new ClipData(new ClipDescription("content",
new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
new ClipData.Item(uri));
sharingIntent.setClipData(clipdata);
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 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
// Make sure pending intents for the system user are still unique across users
// by setting the (otherwise unused) request code to the current user id.
int requestCode = context.getUserId();
// Create a share intent, this will always go through the chooser activity first
// which should not trigger auto-enter PiP
String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
Intent sharingIntent = new Intent(Intent.ACTION_SEND);
sharingIntent.setType("image/png");
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
// Include URI in ClipData also, so that grantPermission picks it up.
// We don't use setData here because some apps interpret this as "to:".
ClipData clipdata = new ClipData(new ClipDescription("content",
new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
new ClipData.Item(uri));
sharingIntent.setClipData(clipdata);
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Intent sharingChooserIntent =
Intent.createChooser(sharingIntent, null)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Make sure pending intents for the system user are still unique across users
// by setting the (otherwise unused) request code to the current user id.
int requestCode = context.getUserId();
// cancel current pending intent (if any) since clipData isn't used for matching
PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
context, 0, sharingChooserIntent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE,
null, UserHandle.CURRENT);
Intent sharingChooserIntent = Intent.createChooser(sharingIntent, null)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Create a share action for the notification
PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, ActionProxyReceiver.class)
.putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent)
.putExtra(ScreenshotController.EXTRA_DISALLOW_ENTER_PIP, true)
.putExtra(ScreenshotController.EXTRA_ID, mScreenshotId)
.putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED,
mSmartActionsEnabled)
.setAction(Intent.ACTION_SEND)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE,
UserHandle.SYSTEM);
// cancel current pending intent (if any) since clipData isn't used for matching
PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
context, 0, sharingChooserIntent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE,
transition.bundle, UserHandle.CURRENT);
Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
Icon.createWithResource(r, R.drawable.ic_screenshot_share),
r.getString(com.android.internal.R.string.share), shareAction);
// Create a share action for the notification
PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, ActionProxyReceiver.class)
.putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent)
.putExtra(ScreenshotController.EXTRA_DISALLOW_ENTER_PIP, true)
.putExtra(ScreenshotController.EXTRA_ID, mScreenshotId)
.putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED,
mSmartActionsEnabled)
.setAction(Intent.ACTION_SEND)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE,
UserHandle.SYSTEM);
return shareActionBuilder.build();
Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
Icon.createWithResource(r, R.drawable.ic_screenshot_share),
r.getString(com.android.internal.R.string.share), shareAction);
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);

View File

@@ -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.
*/

View File

@@ -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,13 +529,31 @@ public class ScreenshotView extends FrameLayout implements
});
return animator;
}
protected View getScreenshotPreview() {
return mScreenshotPreview;
}
void setChipIntents(ScreenshotController.SavedImageData imageData) {
mShareChip.setPendingIntent(imageData.shareAction.actionIntent,
() -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED);
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,
() -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED);
@@ -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");
}

View File

@@ -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(),