Merge "Fetch context URL from the top-most app, send with share if available." into tm-qpr-dev
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<Object, Callback> 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<AssistContentRequester> 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) {}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<OverlayActionChip> 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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user