diff options
12 files changed, 508 insertions, 16 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 2737ecf5ffa6..b5145f926abd 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 000000000000..017e57fcaf62 --- /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 000000000000..5961635a0dba --- /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 000000000000..da834729d319 --- /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 000000000000..e15030f78234 --- /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 f7c4dadc6605..d2e3fbd65762 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 077ad35fd63f..7143ba263570 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 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { 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 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // 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 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { 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 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // 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 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { 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 6d5121a89241..231e415f17c6 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -174,7 +174,7 @@ public class ScreenshotController { public List<Notification.Action> smartActions; public Notification.Action quickShareAction; public UserHandle owner; - + public String subject; // Title for sharing /** * POD for shared element transition. @@ -195,6 +195,7 @@ public class ScreenshotController { deleteAction = null; smartActions = null; quickShareAction = null; + subject = null; } } @@ -273,6 +274,7 @@ public class ScreenshotController { private final ScreenshotNotificationSmartActionsProvider mScreenshotNotificationSmartActionsProvider; private final TimeoutHandler mScreenshotHandler; + private final ActionIntentExecutor mActionExecutor; private ScreenshotView mScreenshotView; private Bitmap mScreenBitmap; @@ -310,7 +312,8 @@ public class ScreenshotController { ActivityManager activityManager, TimeoutHandler timeoutHandler, BroadcastSender broadcastSender, - ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider + ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, + ActionIntentExecutor actionExecutor ) { mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; @@ -340,6 +343,7 @@ public class ScreenshotController { mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); mWindowManager = mContext.getSystemService(WindowManager.class); mFlags = flags; + mActionExecutor = actionExecutor; mAccessibilityManager = AccessibilityManager.getInstance(mContext); @@ -486,7 +490,7 @@ public class ScreenshotController { // 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 000000000000..2e6c7567259f --- /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 793085a60133..c41e2bc14afc 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.content.Intent 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 @@ internal class ScreenshotProxyService @Inject constructor( 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 be41a6b0d376..26cbcbf5214f 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 androidx.constraintlayout.widget.ConstraintLayout; 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 @@ public class ScreenshotView extends FrameLayout implements private final InteractionJankMonitor mInteractionJankMonitor; private long mDefaultTimeoutOfTimeoutHandler; + private ActionIntentExecutor mActionExecutor; + private FeatureFlags mFlags; private enum PendingInteraction { PREVIEW, @@ -422,9 +426,12 @@ public class ScreenshotView extends FrameLayout implements * 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 @@ public class ScreenshotView extends FrameLayout implements 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 000000000000..b6a595b0077a --- /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) + } +} |