Loading packages/SystemUI/AndroidManifest.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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" /> Loading packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt 0 → 100644 +76 −0 Original line number Diff line number Diff line /* * 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) } } packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt 0 → 100644 +159 −0 Original line number Diff line number Diff line /* * 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) {} } packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl 0 → 100644 +27 −0 Original line number Diff line number Diff line /** * 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 packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl 0 → 100644 +21 −0 Original line number Diff line number Diff line /** * 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); } Loading
packages/SystemUI/AndroidManifest.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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" /> Loading
packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt 0 → 100644 +76 −0 Original line number Diff line number Diff line /* * 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) } }
packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt 0 → 100644 +159 −0 Original line number Diff line number Diff line /* * 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) {} }
packages/SystemUI/src/com/android/systemui/screenshot/ICrossProfileService.aidl 0 → 100644 +27 −0 Original line number Diff line number Diff line /** * 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
packages/SystemUI/src/com/android/systemui/screenshot/IOnDoneCallback.aidl 0 → 100644 +21 −0 Original line number Diff line number Diff line /** * 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); }