Fix Chooser-to-editor transition animation

Add a scene animation - image thumbnail in Chooser to full-size image in
the editor - when the thumbnail is visible.
Finishing Chooser activity while the transition animation is running
produces an effect of "flashing" background (as the activity disappears
before the animation completed). Thus the call to finish the activity is
delayed until the activity is stopped (i.e. becomes fully invisible).

Fix: 231448720

Test: Use 'Files' app to share an image, select Edit in the Chooser,
 observe animation (optionally slow down the animation in the developer
 settings)
Test: The same as the previous test but in the split-screen mode.
Test: Slow-down animation, select "edit" in the chooser and cancel the
 animation mid-flight. The ChooserActivity pauses and resumes and
 remains overall functional (had to slow down the fade-out animation in
 the code). The animation in this case is broken but
 addressing this case is beyond the scope of this change.
Test: Use a mock app with the dialog-themed activity instead of the
system image editor; check that Chooser fades out and finishes in
background.
Test: Accessibility with talk-back on.

Change-Id: I982b9b2a8a049b5986dce521ee1abcd9ad83ba38
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 1dedec3..f691300 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -30,6 +30,7 @@
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManager;
+import android.app.ActivityOptions;
 import android.app.SharedElementCallback;
 import android.app.prediction.AppPredictionContext;
 import android.app.prediction.AppPredictionManager;
@@ -101,7 +102,10 @@
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
 import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
 import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.Space;
@@ -197,6 +201,8 @@
     private static final String PLURALS_COUNT = "count";
     private static final String PLURALS_FILE_NAME = "file_name";
 
+    private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
+
     private boolean mIsAppPredictorComponentAvailable;
     private Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache;
     private Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache;
@@ -250,6 +256,11 @@
 
     private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 125;
 
+    private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
+            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+            | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+            | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+
     @VisibleForTesting
     int mListViewUpdateDelayMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
             SystemUiDeviceConfigFlags.SHARESHEET_LIST_VIEW_UPDATE_DELAY,
@@ -305,6 +316,8 @@
 
     private boolean mRemoveSharedElements = false;
 
+    private View mContentView = null;
+
     private class ContentPreviewCoordinator {
         private static final int IMAGE_FADE_IN_MILLIS = 150;
         private static final int IMAGE_LOAD_TIMEOUT = 1;
@@ -990,6 +1003,7 @@
     protected void onResume() {
         super.onResume();
         Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
+        maybeCancelFinishAnimation();
     }
 
     @Override
@@ -1085,6 +1099,10 @@
         final ComponentName cn = getEditSharingComponent();
 
         final Intent resolveIntent = new Intent(originalIntent);
+        // Retain only URI permission grant flags if present. Other flags may prevent the scene
+        // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION,
+        // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed.
+        resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
         resolveIntent.setComponent(cn);
         resolveIntent.setAction(Intent.ACTION_EDIT);
         final ResolveInfo ri = getPackageManager().resolveActivity(
@@ -1101,7 +1119,6 @@
         return dri;
     }
 
-
     @VisibleForTesting
     protected TargetInfo getNearbySharingTarget(Intent originalIntent) {
         final ComponentName cn = getNearbySharingComponent();
@@ -1204,15 +1221,30 @@
                             "",
                             -1,
                             false);
+                    View firstImgView = getFirstVisibleImgPreviewView();
                     // Action bar is user-independent, always start as primary
-                    safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
-                    finish();
+                    if (firstImgView == null) {
+                        safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
+                        finish();
+                    } else {
+                        ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
+                                this, firstImgView, IMAGE_EDITOR_SHARED_ELEMENT);
+                        safelyStartActivityAsUser(
+                                ti, getPersonalProfileUserHandle(), options.toBundle());
+                        startFinishAnimation();
+                    }
                 }
         );
         b.setId(R.id.chooser_edit_button);
         return b;
     }
 
+    @Nullable
+    private View getFirstVisibleImgPreviewView() {
+        View firstImage = findViewById(R.id.content_preview_image_1_large);
+        return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null;
+    }
+
     private void addActionButton(ViewGroup parent, Button b) {
         if (b == null) return;
         final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
@@ -1560,6 +1592,14 @@
     }
 
     @Override
+    protected void onStop() {
+        super.onStop();
+        if (maybeCancelFinishAnimation()) {
+            finish();
+        }
+    }
+
+    @Override
     protected void onDestroy() {
         super.onDestroy();
 
@@ -2887,6 +2927,30 @@
                 .setSubtype(previewType));
     }
 
+    private void startFinishAnimation() {
+        View rootView = findRootView();
+        rootView.startAnimation(new FinishAnimation(this, rootView));
+    }
+
+    private boolean maybeCancelFinishAnimation() {
+        View rootView = findRootView();
+        Animation animation = rootView.getAnimation();
+        if (animation instanceof FinishAnimation) {
+            boolean hasEnded = animation.hasEnded();
+            animation.cancel();
+            rootView.clearAnimation();
+            return !hasEnded;
+        }
+        return false;
+    }
+
+    private View findRootView() {
+        if (mContentView == null) {
+            mContentView = findViewById(android.R.id.content);
+        }
+        return mContentView;
+    }
+
     abstract static class ViewHolderBase extends RecyclerView.ViewHolder {
         private int mViewType;
 
@@ -3987,6 +4051,66 @@
         }
     }
 
+    /**
+     * Used in combination with the scene transition when launching the image editor
+     */
+    private static class FinishAnimation extends AlphaAnimation implements
+            Animation.AnimationListener {
+        private Activity mActivity;
+        private View mRootView;
+        private final float mFromAlpha;
+
+        FinishAnimation(Activity activity, View rootView) {
+            super(rootView.getAlpha(), 0.0f);
+            mActivity = activity;
+            mRootView = rootView;
+            mFromAlpha = rootView.getAlpha();
+            setInterpolator(new LinearInterpolator());
+            long duration = activity.getWindow().getTransitionBackgroundFadeDuration();
+            setDuration(duration);
+            // The scene transition animation looks better when it's not overlapped with this
+            // fade-out animation thus the delay.
+            // It is most likely that the image editor will cause this activity to stop and this
+            // animation will be cancelled in the background without running (i.e. we'll animate
+            // only when this activity remains partially visible after the image editor launch).
+            setStartOffset(duration);
+            super.setAnimationListener(this);
+        }
+
+        @Override
+        public void setAnimationListener(AnimationListener listener) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void cancel() {
+            mRootView.setAlpha(mFromAlpha);
+            cleanup();
+            super.cancel();
+        }
+
+        @Override
+        public void onAnimationStart(Animation animation) {
+        }
+
+        @Override
+        public void onAnimationEnd(Animation animation) {
+            if (mActivity != null) {
+                mActivity.finish();
+                cleanup();
+            }
+        }
+
+        @Override
+        public void onAnimationRepeat(Animation animation) {
+        }
+
+        private void cleanup() {
+            mActivity = null;
+            mRootView = null;
+        }
+    }
+
     @Override
     protected void maybeLogProfileChange() {
         getChooserActivityLogger().logShareheetProfileChanged();
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 40429c6..e9e437f 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -1314,7 +1314,7 @@
         StrictMode.disableDeathOnFileUriExposure();
         try {
             UserHandle currentUserHandle = mMultiProfilePagerAdapter.getCurrentUserHandle();
-            safelyStartActivityInternal(cti, currentUserHandle);
+            safelyStartActivityInternal(cti, currentUserHandle, null);
         } finally {
             StrictMode.enableDeathOnFileUriExposure();
         }
@@ -1327,18 +1327,23 @@
      */
     @VisibleForTesting
     public void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) {
+        safelyStartActivityAsUser(cti, user, null);
+    }
+
+    protected void safelyStartActivityAsUser(
+            TargetInfo cti, UserHandle user, @Nullable Bundle options) {
         // We're dispatching intents that might be coming from legacy apps, so
         // don't kill ourselves.
         StrictMode.disableDeathOnFileUriExposure();
         try {
-            safelyStartActivityInternal(cti, user);
+            safelyStartActivityInternal(cti, user, options);
         } finally {
             StrictMode.enableDeathOnFileUriExposure();
         }
     }
 
-
-    private void safelyStartActivityInternal(TargetInfo cti, UserHandle user) {
+    private void safelyStartActivityInternal(
+            TargetInfo cti, UserHandle user, @Nullable Bundle options) {
         // If the target is suspended, the activity will not be successfully launched.
         // Do not unregister from package manager updates in this case
         if (!cti.isSuspended() && mRegistered) {
@@ -1356,14 +1361,14 @@
             Toast.makeText(this, mProfileSwitchMessage, Toast.LENGTH_LONG).show();
         }
         if (!mSafeForwardingMode) {
-            if (cti.startAsUser(this, null, user)) {
+            if (cti.startAsUser(this, options, user)) {
                 onActivityStarted(cti);
                 maybeLogCrossProfileTargetLaunch(cti, user);
             }
             return;
         }
         try {
-            if (cti.startAsCaller(this, null, user.getIdentifier())) {
+            if (cti.startAsCaller(this, options, user.getIdentifier())) {
                 onActivityStarted(cti);
                 maybeLogCrossProfileTargetLaunch(cti, user);
             }