From 82594cb8e3646b658cf3e5e26a76b1aa861b1fa2 Mon Sep 17 00:00:00 2001 From: Matt Casey Date: Fri, 20 Jan 2023 16:51:26 +0000 Subject: [PATCH] Fetch context URL from the top-most app, send with share if available. - Add contextUrl to ScreenshotData. - Fork AssistContentRequester from launcher, adapt slightly for sysui dagger. - Request the assist content (async) if the task ID is available. - Set the URL as EXTRA_TEXT if available. Don't set the subject in the share if we have EXTRA_TEXT, as they seem to conflict with each other in some apps. - Slight refactor in ActionIntentCreator to make the various ways to create share intents clearer. Flag: All code changes (beyond variable existence) are protected by the SCREENSHOT_METADATA flag, currently disabled. The work profile screenshots flag is also required for this to work properly (in teamfood). Bug: 242791070 Test: atest com.android.systemui.screenshot Test: Verify that context URL from chrome passes all the way through when sharing with Gmail. Merged-In: I39c3503147631f4ef79088f23c201bbad7405d16 Change-Id: I39c3503147631f4ef79088f23c201bbad7405d16 --- .../screenshot/ActionIntentCreator.kt | 17 ++- .../screenshot/AssistContentRequester.java | 144 ++++++++++++++++++ .../screenshot/LongScreenshotActivity.java | 2 +- .../screenshot/ScreenshotController.java | 19 ++- .../systemui/screenshot/ScreenshotData.kt | 3 + .../systemui/screenshot/ScreenshotView.java | 25 ++- .../screenshot/ActionIntentCreatorTest.kt | 48 +++++- 7 files changed, 248 insertions(+), 10 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt index 017e57fcaf62b..310baafbae1a2 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt @@ -25,8 +25,22 @@ import android.net.Uri import com.android.systemui.R object ActionIntentCreator { + /** @return a chooser intent to share the given URI. */ + fun createShareIntent(uri: Uri) = createShareIntent(uri, null, null) + /** @return a chooser intent to share the given URI with the optional provided subject. */ - fun createShareIntent(uri: Uri, subject: String?): Intent { + fun createShareIntentWithSubject(uri: Uri, subject: String?) = + createShareIntent(uri, subject = subject) + + /** @return a chooser intent to share the given URI with the optional provided extra text. */ + fun createShareIntentWithExtraText(uri: Uri, extraText: String?) = + createShareIntent(uri, extraText = extraText) + + private fun createShareIntent( + uri: Uri, + subject: String? = null, + extraText: String? = null + ): Intent { // Create a share intent, this will always go through the chooser activity first // which should not trigger auto-enter PiP val sharingIntent = @@ -43,6 +57,7 @@ object ActionIntentCreator { ) putExtra(Intent.EXTRA_SUBJECT, subject) + putExtra(Intent.EXTRA_TEXT, extraText) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java b/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java new file mode 100644 index 0000000000000..146e5762790cc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; +import android.app.ActivityTaskManager; +import android.app.IActivityTaskManager; +import android.app.IAssistDataReceiver; +import android.app.assist.AssistContent; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; + +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.Executor; + +import javax.inject.Inject; + +/** + * Can be used to request the AssistContent from a provided task id, useful for getting the web uri + * if provided from the task. + * + * Forked from + * packages/apps/Launcher3/quickstep/src/com/android/quickstep/util/AssistContentRequester.java + */ +@SysUISingleton +public class AssistContentRequester { + private static final String TAG = "AssistContentRequester"; + private static final String ASSIST_KEY_CONTENT = "content"; + + /** For receiving content, called on the main thread. */ + public interface Callback { + /** + * Called when the {@link android.app.assist.AssistContent} of the requested task is + * available. + **/ + void onAssistContentAvailable(AssistContent assistContent); + } + + private final IActivityTaskManager mActivityTaskManager; + private final String mPackageName; + private final Executor mCallbackExecutor; + private final Executor mSystemInteractionExecutor; + + // If system loses the callback, our internal cache of original callback will also get cleared. + private final Map mPendingCallbacks = + Collections.synchronizedMap(new WeakHashMap<>()); + + @Inject + public AssistContentRequester(Context context, @Main Executor mainExecutor, + @Background Executor bgExecutor) { + mActivityTaskManager = ActivityTaskManager.getService(); + mPackageName = context.getApplicationContext().getPackageName(); + mCallbackExecutor = mainExecutor; + mSystemInteractionExecutor = bgExecutor; + } + + /** + * Request the {@link AssistContent} from the task with the provided id. + * + * @param taskId to query for the content. + * @param callback to call when the content is available, called on the main thread. + */ + public void requestAssistContent(final int taskId, final Callback callback) { + // ActivityTaskManager interaction here is synchronous, so call off the main thread. + mSystemInteractionExecutor.execute(() -> { + try { + mActivityTaskManager.requestAssistDataForTask( + new AssistDataReceiver(callback, this), taskId, mPackageName); + } catch (RemoteException e) { + Log.e(TAG, "Requesting assist content failed for task: " + taskId, e); + } + }); + } + + private void executeOnMainExecutor(Runnable callback) { + mCallbackExecutor.execute(callback); + } + + private static final class AssistDataReceiver extends IAssistDataReceiver.Stub { + + // The AssistDataReceiver binder callback object is passed to a system server, that may + // keep hold of it for longer than the lifetime of the AssistContentRequester object, + // potentially causing a memory leak. In the callback passed to the system server, only + // keep a weak reference to the parent object and lookup its callback if it still exists. + private final WeakReference mParentRef; + private final Object mCallbackKey = new Object(); + + AssistDataReceiver(Callback callback, AssistContentRequester parent) { + parent.mPendingCallbacks.put(mCallbackKey, callback); + mParentRef = new WeakReference<>(parent); + } + + @Override + public void onHandleAssistData(Bundle data) { + if (data == null) { + return; + } + + final AssistContent content = data.getParcelable(ASSIST_KEY_CONTENT); + if (content == null) { + Log.e(TAG, "Received AssistData, but no AssistContent found"); + return; + } + + AssistContentRequester requester = mParentRef.get(); + if (requester != null) { + Callback callback = requester.mPendingCallbacks.get(mCallbackKey); + if (callback != null) { + requester.executeOnMainExecutor( + () -> callback.onAssistContentAvailable(content)); + } else { + Log.d(TAG, "Callback received after calling UI was disposed of"); + } + } else { + Log.d(TAG, "Callback received after Requester was collected"); + } + } + + @Override + public void onHandleAssistScreenshot(Bitmap screenshot) {} + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java index d64b33bb3eb12..ca8e10176e7f6 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java @@ -366,7 +366,7 @@ public class LongScreenshotActivity extends Activity { private void doShare(Uri uri) { if (mFeatureFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) { - Intent shareIntent = ActionIntentCreator.INSTANCE.createShareIntent(uri, null); + Intent shareIntent = ActionIntentCreator.INSTANCE.createShareIntent(uri); mActionExecutor.launchIntentAsync(shareIntent, null, mScreenshotUserHandle.getIdentifier(), false); } else { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 02719e3fc5c00..adff6e1757f6e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -44,6 +44,7 @@ import android.app.ExitTransitionCoordinator; import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks; import android.app.ICompatCameraControlCallback; import android.app.Notification; +import android.app.assist.AssistContent; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -281,6 +282,7 @@ public class ScreenshotController { private final ActionIntentExecutor mActionExecutor; private final UserManager mUserManager; private final WorkProfileMessageController mWorkProfileMessageController; + private final AssistContentRequester mAssistContentRequester; private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { if (DEBUG_INPUT) { @@ -328,7 +330,8 @@ public class ScreenshotController { ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, ActionIntentExecutor actionExecutor, UserManager userManager, - WorkProfileMessageController workProfileMessageController + WorkProfileMessageController workProfileMessageController, + AssistContentRequester assistContentRequester ) { mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; @@ -361,6 +364,7 @@ public class ScreenshotController { mActionExecutor = actionExecutor; mUserManager = userManager; mWorkProfileMessageController = workProfileMessageController; + mAssistContentRequester = assistContentRequester; mAccessibilityManager = AccessibilityManager.getInstance(mContext); @@ -466,7 +470,18 @@ public class ScreenshotController { mContext.getDrawable(R.drawable.overlay_badge_background), screenshot.getUserHandle())); } - mScreenshotView.setScreenshot(mScreenBitmap, screenshot.getInsets()); + mScreenshotView.setScreenshot(screenshot); + + if (screenshot.getTaskId() >= 0) { + mAssistContentRequester.requestAssistContent(screenshot.getTaskId(), + new AssistContentRequester.Callback() { + @Override + public void onAssistContentAvailable(AssistContent assistContent) { + screenshot.setContextUrl(assistContent.getWebUri()); + } + }); + } + if (DEBUG_WINDOW) { Log.d(TAG, "setContentView: " + mScreenshotView); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt index 1d1b968252609..c43e4b4e3f3a3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt @@ -4,6 +4,7 @@ import android.content.ComponentName import android.graphics.Bitmap import android.graphics.Insets import android.graphics.Rect +import android.net.Uri import android.os.UserHandle import android.view.WindowManager.ScreenshotSource import android.view.WindowManager.ScreenshotType @@ -21,6 +22,8 @@ data class ScreenshotData( var taskId: Int, var insets: Insets, var bitmap: Bitmap?, + /** App-provided URL representing the content the user was looking at in the screenshot. */ + var contextUrl: Uri? = null, ) { val packageNameString: String get() = if (topComponent == null) "" else topComponent!!.packageName diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 200a7dc08bb3b..ce7f2e72a54d1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -38,6 +38,7 @@ import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Bitmap; @@ -166,6 +167,8 @@ public class ScreenshotView extends FrameLayout implements private final ArrayList mSmartChips = new ArrayList<>(); private PendingInteraction mPendingInteraction; + // Should only be set/used if the SCREENSHOT_METADATA flag is set. + private ScreenshotData mScreenshotData; private final InteractionJankMonitor mInteractionJankMonitor; private long mDefaultTimeoutOfTimeoutHandler; @@ -470,6 +473,13 @@ public class ScreenshotView extends FrameLayout implements mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets)); } + void setScreenshot(ScreenshotData screenshot) { + mScreenshotData = screenshot; + setScreenshot(screenshot.getBitmap(), screenshot.getInsets()); + mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, screenshot.getBitmap(), + screenshot.getInsets())); + } + void setPackageName(String packageName) { mPackageName = packageName; } @@ -808,9 +818,17 @@ public class ScreenshotView extends FrameLayout implements mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName); if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) { prepareSharedTransition(); - mActionExecutor.launchIntentAsync( - ActionIntentCreator.INSTANCE.createShareIntent( - imageData.uri, imageData.subject), + + Intent shareIntent; + if (mFlags.isEnabled(Flags.SCREENSHOT_METADATA) && mScreenshotData != null + && mScreenshotData.getContextUrl() != null) { + shareIntent = ActionIntentCreator.INSTANCE.createShareIntentWithExtraText( + imageData.uri, mScreenshotData.getContextUrl().toString()); + } else { + shareIntent = ActionIntentCreator.INSTANCE.createShareIntentWithSubject( + imageData.uri, imageData.subject); + } + mActionExecutor.launchIntentAsync(shareIntent, imageData.shareTransition.get().bundle, imageData.owner.getIdentifier(), false); } else { @@ -1112,6 +1130,7 @@ public class ScreenshotView extends FrameLayout implements mQuickShareChip = null; setAlpha(1); mScreenshotStatic.setAlpha(1); + mScreenshotData = null; } private void startSharedTransition(ActionTransition transition) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt index b6a595b0077a3..7ba2cf7f6374b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt @@ -35,9 +35,33 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test fun testCreateShareIntent() { val uri = Uri.parse("content://fake") + + val output = ActionIntentCreator.createShareIntent(uri) + + assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER) + assertFlagsSet( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_GRANT_READ_URI_PERMISSION, + output.flags + ) + + val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + assertThat(wrappedIntent?.action).isEqualTo(Intent.ACTION_SEND) + assertThat(wrappedIntent?.data).isEqualTo(uri) + assertThat(wrappedIntent?.type).isEqualTo("image/png") + assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isNull() + assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_TEXT)).isNull() + assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)) + .isEqualTo(uri) + } + + @Test + fun testCreateShareIntentWithSubject() { + val uri = Uri.parse("content://fake") val subject = "Example subject" - val output = ActionIntentCreator.createShareIntent(uri, subject) + val output = ActionIntentCreator.createShareIntentWithSubject(uri, subject) assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER) assertFlagsSet( @@ -52,16 +76,34 @@ class ActionIntentCreatorTest : SysuiTestCase() { assertThat(wrappedIntent?.data).isEqualTo(uri) assertThat(wrappedIntent?.type).isEqualTo("image/png") assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isEqualTo(subject) + assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_TEXT)).isNull() assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)) .isEqualTo(uri) } @Test - fun testCreateShareIntent_noSubject() { + fun testCreateShareIntentWithExtraText() { val uri = Uri.parse("content://fake") - val output = ActionIntentCreator.createShareIntent(uri, null) + val extraText = "Extra text" + + val output = ActionIntentCreator.createShareIntentWithExtraText(uri, extraText) + + assertThat(output.action).isEqualTo(Intent.ACTION_CHOOSER) + assertFlagsSet( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_GRANT_READ_URI_PERMISSION, + output.flags + ) + val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + assertThat(wrappedIntent?.action).isEqualTo(Intent.ACTION_SEND) + assertThat(wrappedIntent?.data).isEqualTo(uri) + assertThat(wrappedIntent?.type).isEqualTo("image/png") assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isNull() + assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(extraText) + assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)) + .isEqualTo(uri) } @Test