Merge "Fix share/edit actions for work profile." into tm-qpr-dev am: 693d0a4131
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20141373 Change-Id: I9ac90d4f0f310a3d313cfcad26d841773b01a387 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -402,6 +402,9 @@
|
|||||||
android:permission="com.android.systemui.permission.SELF"
|
android:permission="com.android.systemui.permission.SELF"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service android:name=".screenshot.ScreenshotCrossProfileService"
|
||||||
|
android:permission="com.android.systemui.permission.SELF"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".screenrecord.RecordingService" />
|
<service android:name=".screenrecord.RecordingService" />
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -16,9 +16,14 @@
|
|||||||
|
|
||||||
package com.android.systemui.screenshot;
|
package com.android.systemui.screenshot;
|
||||||
|
|
||||||
|
import com.android.systemui.screenshot.IOnDoneCallback;
|
||||||
|
|
||||||
/** Interface implemented by ScreenshotProxyService */
|
/** Interface implemented by ScreenshotProxyService */
|
||||||
interface IScreenshotProxy {
|
interface IScreenshotProxy {
|
||||||
|
|
||||||
/** Is the notification shade currently exanded? */
|
/** Is the notification shade currently exanded? */
|
||||||
boolean isNotificationShadeExpanded();
|
boolean isNotificationShadeExpanded();
|
||||||
|
|
||||||
|
/** Attempts to dismiss the keyguard. */
|
||||||
|
void dismissKeyguard(IOnDoneCallback callback);
|
||||||
}
|
}
|
||||||
@@ -173,6 +173,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
|
|||||||
mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri);
|
mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri);
|
||||||
mImageData.quickShareAction = createQuickShareAction(mContext,
|
mImageData.quickShareAction = createQuickShareAction(mContext,
|
||||||
mQuickShareData.quickShareAction, uri);
|
mQuickShareData.quickShareAction, uri);
|
||||||
|
mImageData.subject = getSubjectString();
|
||||||
|
|
||||||
mParams.mActionsReadyListener.onActionsReady(mImageData);
|
mParams.mActionsReadyListener.onActionsReady(mImageData);
|
||||||
if (DEBUG_CALLBACK) {
|
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
|
// Create a share intent, this will always go through the chooser activity first
|
||||||
// which should not trigger auto-enter PiP
|
// 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);
|
Intent sharingIntent = new Intent(Intent.ACTION_SEND);
|
||||||
sharingIntent.setDataAndType(uri, "image/png");
|
sharingIntent.setDataAndType(uri, "image/png");
|
||||||
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
|
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||||
@@ -248,7 +247,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
|
|||||||
new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
|
new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
|
||||||
new ClipData.Item(uri));
|
new ClipData.Item(uri));
|
||||||
sharingIntent.setClipData(clipdata);
|
sharingIntent.setClipData(clipdata);
|
||||||
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, getSubjectString());
|
||||||
sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
.addFlags(Intent.FLAG_GRANT_WRITE_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.
|
// by setting the (otherwise unused) request code to the current user id.
|
||||||
int requestCode = mContext.getUserId();
|
int requestCode = mContext.getUserId();
|
||||||
|
|
||||||
// Create a edit action
|
// Create an edit action
|
||||||
PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
|
PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
|
||||||
new Intent(context, ActionProxyReceiver.class)
|
new Intent(context, ActionProxyReceiver.class)
|
||||||
.putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent)
|
.putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent)
|
||||||
@@ -479,4 +478,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
|
|||||||
mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
|
mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getSubjectString() {
|
||||||
|
String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
|
||||||
|
return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ public class ScreenshotController {
|
|||||||
public List<Notification.Action> smartActions;
|
public List<Notification.Action> smartActions;
|
||||||
public Notification.Action quickShareAction;
|
public Notification.Action quickShareAction;
|
||||||
public UserHandle owner;
|
public UserHandle owner;
|
||||||
|
public String subject; // Title for sharing
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POD for shared element transition.
|
* POD for shared element transition.
|
||||||
@@ -195,6 +195,7 @@ public class ScreenshotController {
|
|||||||
deleteAction = null;
|
deleteAction = null;
|
||||||
smartActions = null;
|
smartActions = null;
|
||||||
quickShareAction = null;
|
quickShareAction = null;
|
||||||
|
subject = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +274,7 @@ public class ScreenshotController {
|
|||||||
private final ScreenshotNotificationSmartActionsProvider
|
private final ScreenshotNotificationSmartActionsProvider
|
||||||
mScreenshotNotificationSmartActionsProvider;
|
mScreenshotNotificationSmartActionsProvider;
|
||||||
private final TimeoutHandler mScreenshotHandler;
|
private final TimeoutHandler mScreenshotHandler;
|
||||||
|
private final ActionIntentExecutor mActionExecutor;
|
||||||
|
|
||||||
private ScreenshotView mScreenshotView;
|
private ScreenshotView mScreenshotView;
|
||||||
private Bitmap mScreenBitmap;
|
private Bitmap mScreenBitmap;
|
||||||
@@ -310,7 +312,8 @@ public class ScreenshotController {
|
|||||||
ActivityManager activityManager,
|
ActivityManager activityManager,
|
||||||
TimeoutHandler timeoutHandler,
|
TimeoutHandler timeoutHandler,
|
||||||
BroadcastSender broadcastSender,
|
BroadcastSender broadcastSender,
|
||||||
ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider
|
ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
|
||||||
|
ActionIntentExecutor actionExecutor
|
||||||
) {
|
) {
|
||||||
mScreenshotSmartActions = screenshotSmartActions;
|
mScreenshotSmartActions = screenshotSmartActions;
|
||||||
mNotificationsController = screenshotNotificationsController;
|
mNotificationsController = screenshotNotificationsController;
|
||||||
@@ -340,6 +343,7 @@ public class ScreenshotController {
|
|||||||
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
|
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
|
||||||
mWindowManager = mContext.getSystemService(WindowManager.class);
|
mWindowManager = mContext.getSystemService(WindowManager.class);
|
||||||
mFlags = flags;
|
mFlags = flags;
|
||||||
|
mActionExecutor = actionExecutor;
|
||||||
|
|
||||||
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
|
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
|
||||||
|
|
||||||
@@ -486,7 +490,7 @@ public class ScreenshotController {
|
|||||||
// TODO(159460485): Remove this when focus is handled properly in the system
|
// TODO(159460485): Remove this when focus is handled properly in the system
|
||||||
setWindowFocusable(false);
|
setWindowFocusable(false);
|
||||||
}
|
}
|
||||||
});
|
}, mActionExecutor, mFlags);
|
||||||
mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());
|
mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());
|
||||||
|
|
||||||
mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
|
mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,13 +20,16 @@ import android.content.Intent
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.android.systemui.shade.ShadeExpansionStateManager
|
import com.android.systemui.shade.ShadeExpansionStateManager
|
||||||
|
import com.android.systemui.statusbar.phone.CentralSurfaces
|
||||||
|
import java.util.Optional
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides state from the main SystemUI process on behalf of the Screenshot process.
|
* Provides state from the main SystemUI process on behalf of the Screenshot process.
|
||||||
*/
|
*/
|
||||||
internal class ScreenshotProxyService @Inject constructor(
|
internal class ScreenshotProxyService @Inject constructor(
|
||||||
private val mExpansionMgr: ShadeExpansionStateManager
|
private val mExpansionMgr: ShadeExpansionStateManager,
|
||||||
|
private val mCentralSurfacesOptional: Optional<CentralSurfaces>,
|
||||||
) : Service() {
|
) : Service() {
|
||||||
|
|
||||||
private val mBinder: IBinder = object : IScreenshotProxy.Stub() {
|
private val mBinder: IBinder = object : IScreenshotProxy.Stub() {
|
||||||
@@ -38,6 +41,20 @@ internal class ScreenshotProxyService @Inject constructor(
|
|||||||
Log.d(TAG, "isNotificationShadeExpanded(): $expanded")
|
Log.d(TAG, "isNotificationShadeExpanded(): $expanded")
|
||||||
return 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? {
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ import androidx.constraintlayout.widget.ConstraintLayout;
|
|||||||
import com.android.internal.jank.InteractionJankMonitor;
|
import com.android.internal.jank.InteractionJankMonitor;
|
||||||
import com.android.internal.logging.UiEventLogger;
|
import com.android.internal.logging.UiEventLogger;
|
||||||
import com.android.systemui.R;
|
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.screenshot.ScreenshotController.SavedImageData.ActionTransition;
|
||||||
import com.android.systemui.shared.system.InputChannelCompat;
|
import com.android.systemui.shared.system.InputChannelCompat;
|
||||||
import com.android.systemui.shared.system.InputMonitorCompat;
|
import com.android.systemui.shared.system.InputMonitorCompat;
|
||||||
@@ -168,6 +170,8 @@ public class ScreenshotView extends FrameLayout implements
|
|||||||
|
|
||||||
private final InteractionJankMonitor mInteractionJankMonitor;
|
private final InteractionJankMonitor mInteractionJankMonitor;
|
||||||
private long mDefaultTimeoutOfTimeoutHandler;
|
private long mDefaultTimeoutOfTimeoutHandler;
|
||||||
|
private ActionIntentExecutor mActionExecutor;
|
||||||
|
private FeatureFlags mFlags;
|
||||||
|
|
||||||
private enum PendingInteraction {
|
private enum PendingInteraction {
|
||||||
PREVIEW,
|
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
|
* Note: must be called before any other (non-constructor) method or null pointer exceptions
|
||||||
* may occur.
|
* may occur.
|
||||||
*/
|
*/
|
||||||
void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks) {
|
void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks,
|
||||||
|
ActionIntentExecutor actionExecutor, FeatureFlags flags) {
|
||||||
mUiEventLogger = uiEventLogger;
|
mUiEventLogger = uiEventLogger;
|
||||||
mCallbacks = callbacks;
|
mCallbacks = callbacks;
|
||||||
|
mActionExecutor = actionExecutor;
|
||||||
|
mFlags = flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setScreenshot(Bitmap bitmap, Insets screenInsets) {
|
void setScreenshot(Bitmap bitmap, Insets screenInsets) {
|
||||||
@@ -759,18 +766,37 @@ public class ScreenshotView extends FrameLayout implements
|
|||||||
void setChipIntents(ScreenshotController.SavedImageData imageData) {
|
void setChipIntents(ScreenshotController.SavedImageData imageData) {
|
||||||
mShareChip.setOnClickListener(v -> {
|
mShareChip.setOnClickListener(v -> {
|
||||||
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
|
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
|
||||||
startSharedTransition(
|
if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
|
||||||
imageData.shareTransition.get());
|
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 -> {
|
mEditChip.setOnClickListener(v -> {
|
||||||
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
|
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
|
||||||
startSharedTransition(
|
if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
|
||||||
imageData.editTransition.get());
|
mActionExecutor.launchIntentAsync(
|
||||||
|
ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
|
||||||
|
imageData.editTransition.get().bundle,
|
||||||
|
imageData.owner.getIdentifier(), true);
|
||||||
|
} else {
|
||||||
|
startSharedTransition(imageData.editTransition.get());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
mScreenshotPreview.setOnClickListener(v -> {
|
mScreenshotPreview.setOnClickListener(v -> {
|
||||||
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
|
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
|
||||||
|
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(
|
startSharedTransition(
|
||||||
imageData.editTransition.get());
|
imageData.editTransition.get());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (mQuickShareChip != null) {
|
if (mQuickShareChip != null) {
|
||||||
mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent,
|
mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user