Fix share/edit actions for work profile.
In order to be able to proxy the permissions to read/write the
screenshot when editing/sharing, the intents must be launched from a
service running *as* the work profile user (if the screenshot is work
profile). That is done with ScreenshotProxyService.
In order to untangle action execution to accomodate this, I created
ActionIntentExecutor, which does the following:
1. Dismisses the keyguard
2. Launches the intent (either directly if it's the main user, or
delegating to the cross profile service if it's not)
3. Override some transition logic for shared element.
This flow is used whenever the work profile flag is set (it works for
either profile, but is required for work profile).
Once we switch to this model, we can drop a lot of the
SaveImageInBackgroundTask action code, drop ActionProxyReceiver entirely,
etc.
Extracted intent creation to its own class so it could be tested.
All behavior changes covered by currently-off flags.
Known issues:
1. This doesn't fix smart actions
2. If you do an action on the lock screen, the bouncer doesn't come up
in the new implementation. I verified that the same call is being
made and it's even getting to the bouncer's show() call but haven't
figured out what's wrong here yet.
Bug: 231957192
Test: manual sharing and editing screenshtos of wp and non-wp apps.
Change-Id: Id2b423c22d209fc73f796ed6ba6e3d153e0f638e
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 2737ecf..b5145f9 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -402,6 +402,9 @@
android:permission="com.android.systemui.permission.SELF"
android:exported="false" />
+ <service android:name=".screenshot.ScreenshotCrossProfileService"
+ android:permission="com.android.systemui.permission.SELF"
+ android:exported="false" />
<service android:name=".screenrecord.RecordingService" />
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
new file mode 100644
index 0000000..017e57f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.content.ClipData
+import android.content.ClipDescription
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import com.android.systemui.R
+
+object ActionIntentCreator {
+ /** @return a chooser intent to share the given URI with the optional provided subject. */
+ fun createShareIntent(uri: Uri, subject: String?): Intent {
+ // Create a share intent, this will always go through the chooser activity first
+ // which should not trigger auto-enter PiP
+ val sharingIntent =
+ Intent(Intent.ACTION_SEND).apply {
+ setDataAndType(uri, "image/png")
+ 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(
+ ClipDescription("content", arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)),
+ ClipData.Item(uri)
+ )
+
+ putExtra(Intent.EXTRA_SUBJECT, subject)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ }
+
+ return Intent.createChooser(sharingIntent, null)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+
+ /**
+ * @return an ACTION_EDIT intent for the given URI, directed to config_screenshotEditor if
+ * available.
+ */
+ fun createEditIntent(uri: Uri, context: Context): Intent {
+ val editIntent = Intent(Intent.ACTION_EDIT)
+
+ context.getString(R.string.config_screenshotEditor)?.let {
+ if (it.isNotEmpty()) {
+ editIntent.component = ComponentName.unflattenFromString(it)
+ }
+ }
+
+ return editIntent
+ .setDataAndType(uri, "image/png")
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
new file mode 100644
index 0000000..5961635
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.RemoteException
+import android.os.UserHandle
+import android.util.Log
+import android.view.Display
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationAdapter
+import android.view.RemoteAnimationTarget
+import android.view.WindowManager
+import android.view.WindowManagerGlobal
+import com.android.internal.infra.ServiceConnector
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class ActionIntentExecutor
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val context: Context,
+) {
+ /**
+ * Execute the given intent with startActivity while performing operations for screenshot action
+ * launching.
+ * - Dismiss the keyguard first
+ * - If the userId is not the current user, proxy to a service running as that user to execute
+ * - After startActivity, optionally override the pending app transition.
+ */
+ fun launchIntentAsync(
+ intent: Intent,
+ bundle: Bundle,
+ userId: Int,
+ overrideTransition: Boolean,
+ ) {
+ applicationScope.launch { launchIntent(intent, bundle, userId, overrideTransition) }
+ }
+
+ suspend fun launchIntent(
+ intent: Intent,
+ bundle: Bundle,
+ userId: Int,
+ overrideTransition: Boolean,
+ ) {
+ withContext(bgDispatcher) {
+ dismissKeyguard()
+
+ if (userId == UserHandle.myUserId()) {
+ context.startActivity(intent, bundle)
+ } else {
+ launchCrossProfileIntent(userId, intent, bundle)
+ }
+
+ if (overrideTransition) {
+ val runner = RemoteAnimationAdapter(SCREENSHOT_REMOTE_RUNNER, 0, 0)
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .overridePendingAppTransitionRemote(runner, Display.DEFAULT_DISPLAY)
+ } catch (e: Exception) {
+ Log.e(TAG, "Error overriding screenshot app transition", e)
+ }
+ }
+ }
+ }
+
+ private val proxyConnector: ServiceConnector<IScreenshotProxy> =
+ ServiceConnector.Impl(
+ context,
+ Intent(context, ScreenshotProxyService::class.java),
+ Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+ context.userId,
+ IScreenshotProxy.Stub::asInterface,
+ )
+
+ private suspend fun dismissKeyguard() {
+ val completion = CompletableDeferred<Unit>()
+ val onDoneBinder =
+ object : IOnDoneCallback.Stub() {
+ override fun onDone(success: Boolean) {
+ completion.complete(Unit)
+ }
+ }
+ proxyConnector.post { it.dismissKeyguard(onDoneBinder) }
+ completion.await()
+ }
+
+ private fun getCrossProfileConnector(userId: Int): ServiceConnector<ICrossProfileService> =
+ ServiceConnector.Impl<ICrossProfileService>(
+ context,
+ Intent(context, ScreenshotCrossProfileService::class.java),
+ Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+ userId,
+ ICrossProfileService.Stub::asInterface,
+ )
+
+ private suspend fun launchCrossProfileIntent(userId: Int, intent: Intent, bundle: Bundle) {
+ val connector = getCrossProfileConnector(userId)
+ val completion = CompletableDeferred<Unit>()
+ connector.post {
+ it.launchIntent(intent, bundle)
+ completion.complete(Unit)
+ }
+ completion.await()
+ }
+}
+
+private const val TAG: String = "ActionIntentExecutor"
+private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"
+
+/**
+ * This is effectively a no-op, but we need something non-null to pass in, in order to successfully
+ * override the pending activity entrance animation.
+ */
+private val SCREENSHOT_REMOTE_RUNNER: IRemoteAnimationRunner.Stub =
+ object : IRemoteAnimationRunner.Stub() {
+ override fun onAnimationStart(
+ @WindowManager.TransitionOldType transit: Int,
+ apps: Array<RemoteAnimationTarget>,
+ wallpapers: Array<RemoteAnimationTarget>,
+ nonApps: Array<RemoteAnimationTarget>,
+ finishedCallback: IRemoteAnimationFinishedCallback,
+ ) {
+ try {
+ finishedCallback.onAnimationFinished()
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Error finishing screenshot remote animation", e)
+ }
+ }
+
+ override fun onAnimationCancelled(isKeyguardOccluded: Boolean) {}
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl
new file mode 100644
index 0000000..da83472
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2009, 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.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+
+/** Interface implemented by ScreenshotCrossProfileService */
+interface ICrossProfileService {
+
+ void launchIntent(in Intent intent, in Bundle bundle);
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl
new file mode 100644
index 0000000..e15030f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl
@@ -0,0 +1,21 @@
+/**
+ * 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;
+
+interface IOnDoneCallback {
+ void onDone(boolean success);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
index f7c4dad..d2e3fbd 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
@@ -16,9 +16,14 @@
package com.android.systemui.screenshot;
+import com.android.systemui.screenshot.IOnDoneCallback;
+
/** Interface implemented by ScreenshotProxyService */
interface IScreenshotProxy {
/** Is the notification shade currently exanded? */
boolean isNotificationShadeExpanded();
-}
\ No newline at end of file
+
+ /** Attempts to dismiss the keyguard. */
+ void dismissKeyguard(IOnDoneCallback callback);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index 077ad35..7143ba2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -173,6 +173,7 @@
mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri);
mImageData.quickShareAction = createQuickShareAction(mContext,
mQuickShareData.quickShareAction, uri);
+ mImageData.subject = getSubjectString();
mParams.mActionsReadyListener.onActionsReady(mImageData);
if (DEBUG_CALLBACK) {
@@ -237,8 +238,6 @@
// 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.setDataAndType(uri, "image/png");
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
@@ -248,7 +247,7 @@
new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
new ClipData.Item(uri));
sharingIntent.setClipData(clipdata);
- sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
+ sharingIntent.putExtra(Intent.EXTRA_SUBJECT, getSubjectString());
sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
@@ -318,7 +317,7 @@
// by setting the (otherwise unused) request code to the current user id.
int requestCode = mContext.getUserId();
- // Create a edit action
+ // Create an edit action
PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, ActionProxyReceiver.class)
.putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent)
@@ -479,4 +478,9 @@
mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
}
}
+
+ private String getSubjectString() {
+ String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
+ return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 704e115..1c4af1d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -173,7 +173,7 @@
public List<Notification.Action> smartActions;
public Notification.Action quickShareAction;
public UserHandle owner;
-
+ public String subject; // Title for sharing
/**
* POD for shared element transition.
@@ -194,6 +194,7 @@
deleteAction = null;
smartActions = null;
quickShareAction = null;
+ subject = null;
}
}
@@ -272,6 +273,7 @@
private final ScreenshotNotificationSmartActionsProvider
mScreenshotNotificationSmartActionsProvider;
private final TimeoutHandler mScreenshotHandler;
+ private final ActionIntentExecutor mActionExecutor;
private ScreenshotView mScreenshotView;
private Bitmap mScreenBitmap;
@@ -309,7 +311,8 @@
ActivityManager activityManager,
TimeoutHandler timeoutHandler,
BroadcastSender broadcastSender,
- ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider
+ ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
+ ActionIntentExecutor actionExecutor
) {
mScreenshotSmartActions = screenshotSmartActions;
mNotificationsController = screenshotNotificationsController;
@@ -341,6 +344,7 @@
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mWindowManager = mContext.getSystemService(WindowManager.class);
mFlags = flags;
+ mActionExecutor = actionExecutor;
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
@@ -492,7 +496,7 @@
// TODO(159460485): Remove this when focus is handled properly in the system
setWindowFocusable(false);
}
- });
+ }, mActionExecutor, mFlags);
mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());
mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt
new file mode 100644
index 0000000..2e6c756
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotCrossProfileService.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.Service
+import android.content.Intent
+import android.os.Bundle
+import android.os.IBinder
+import android.util.Log
+
+/**
+ * If a screenshot is saved to the work profile, any intents that grant access to the screenshot
+ * must come from a service running as the work profile user. This service is meant to be started as
+ * the desired user and just startActivity for the given intent.
+ */
+class ScreenshotCrossProfileService : Service() {
+
+ private val mBinder: IBinder =
+ object : ICrossProfileService.Stub() {
+ override fun launchIntent(intent: Intent, bundle: Bundle) {
+ startActivity(intent, bundle)
+ }
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ Log.d(TAG, "onBind: $intent")
+ return mBinder
+ }
+
+ companion object {
+ const val TAG = "ScreenshotProxyService"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
index 793085a..c41e2bc 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -20,13 +20,16 @@
import android.os.IBinder
import android.util.Log
import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.statusbar.phone.CentralSurfaces
+import java.util.Optional
import javax.inject.Inject
/**
* Provides state from the main SystemUI process on behalf of the Screenshot process.
*/
internal class ScreenshotProxyService @Inject constructor(
- private val mExpansionMgr: ShadeExpansionStateManager
+ private val mExpansionMgr: ShadeExpansionStateManager,
+ private val mCentralSurfacesOptional: Optional<CentralSurfaces>,
) : Service() {
private val mBinder: IBinder = object : IScreenshotProxy.Stub() {
@@ -38,6 +41,20 @@
Log.d(TAG, "isNotificationShadeExpanded(): $expanded")
return expanded
}
+
+ override fun dismissKeyguard(callback: IOnDoneCallback) {
+ if (mCentralSurfacesOptional.isPresent) {
+ mCentralSurfacesOptional.get().executeRunnableDismissingKeyguard(
+ Runnable {
+ callback.onDone(true)
+ }, null,
+ true /* dismissShade */, true /* afterKeyguardGone */,
+ true /* deferred */
+ )
+ } else {
+ callback.onDone(false)
+ }
+ }
}
override fun onBind(intent: Intent): IBinder? {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index be41a6b..26cbcbf 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -87,6 +87,8 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.shared.system.InputMonitorCompat;
@@ -168,6 +170,8 @@
private final InteractionJankMonitor mInteractionJankMonitor;
private long mDefaultTimeoutOfTimeoutHandler;
+ private ActionIntentExecutor mActionExecutor;
+ private FeatureFlags mFlags;
private enum PendingInteraction {
PREVIEW,
@@ -422,9 +426,12 @@
* Note: must be called before any other (non-constructor) method or null pointer exceptions
* may occur.
*/
- void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks) {
+ void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks,
+ ActionIntentExecutor actionExecutor, FeatureFlags flags) {
mUiEventLogger = uiEventLogger;
mCallbacks = callbacks;
+ mActionExecutor = actionExecutor;
+ mFlags = flags;
}
void setScreenshot(Bitmap bitmap, Insets screenInsets) {
@@ -759,18 +766,37 @@
void setChipIntents(ScreenshotController.SavedImageData imageData) {
mShareChip.setOnClickListener(v -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
- startSharedTransition(
- imageData.shareTransition.get());
+ if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ mActionExecutor.launchIntentAsync(ActionIntentCreator.INSTANCE.createShareIntent(
+ imageData.uri, imageData.subject),
+ imageData.shareTransition.get().bundle,
+ imageData.owner.getIdentifier(), false);
+ } else {
+ startSharedTransition(imageData.shareTransition.get());
+ }
});
mEditChip.setOnClickListener(v -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
- startSharedTransition(
- imageData.editTransition.get());
+ if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ mActionExecutor.launchIntentAsync(
+ ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
+ imageData.editTransition.get().bundle,
+ imageData.owner.getIdentifier(), true);
+ } else {
+ startSharedTransition(imageData.editTransition.get());
+ }
});
mScreenshotPreview.setOnClickListener(v -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
- startSharedTransition(
- imageData.editTransition.get());
+ if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+ mActionExecutor.launchIntentAsync(
+ ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
+ imageData.editTransition.get().bundle,
+ imageData.owner.getIdentifier(), true);
+ } else {
+ startSharedTransition(
+ imageData.editTransition.get());
+ }
});
if (mQuickShareChip != null) {
mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt
new file mode 100644
index 0000000..b6a595b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+class ActionIntentCreatorTest : SysuiTestCase() {
+
+ @Test
+ fun testCreateShareIntent() {
+ val uri = Uri.parse("content://fake")
+ val subject = "Example subject"
+
+ val output = ActionIntentCreator.createShareIntent(uri, subject)
+
+ 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)).isEqualTo(subject)
+ assertThat(wrappedIntent?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java))
+ .isEqualTo(uri)
+ }
+
+ @Test
+ fun testCreateShareIntent_noSubject() {
+ val uri = Uri.parse("content://fake")
+ val output = ActionIntentCreator.createShareIntent(uri, null)
+ val wrappedIntent = output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
+ assertThat(wrappedIntent?.getStringExtra(Intent.EXTRA_SUBJECT)).isNull()
+ }
+
+ @Test
+ fun testCreateEditIntent() {
+ val uri = Uri.parse("content://fake")
+ val context = mock<Context>()
+
+ val output = ActionIntentCreator.createEditIntent(uri, context)
+
+ assertThat(output.action).isEqualTo(Intent.ACTION_EDIT)
+ assertThat(output.data).isEqualTo(uri)
+ assertThat(output.type).isEqualTo("image/png")
+ assertThat(output.component).isNull()
+ val expectedFlags =
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
+ Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_CLEAR_TASK
+ assertFlagsSet(expectedFlags, output.flags)
+ }
+
+ @Test
+ fun testCreateEditIntent_withEditor() {
+ val uri = Uri.parse("content://fake")
+ val context = mock<Context>()
+ var component = ComponentName("com.android.foo", "com.android.foo.Something")
+
+ whenever(context.getString(eq(R.string.config_screenshotEditor)))
+ .thenReturn(component.flattenToString())
+
+ val output = ActionIntentCreator.createEditIntent(uri, context)
+
+ assertThat(output.component).isEqualTo(component)
+ }
+
+ private fun assertFlagsSet(expected: Int, observed: Int) {
+ assertThat(observed and expected).isEqualTo(expected)
+ }
+}