1 /*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.screenshot
18
19 import android.content.ClipData
20 import android.content.ClipDescription
21 import android.content.ComponentName
22 import android.content.ContentProvider
23 import android.content.Context
24 import android.content.Intent
25 import android.content.pm.PackageManager
26 import android.content.pm.PackageManager.NameNotFoundException
27 import android.net.Uri
28 import android.os.UserHandle
29 import com.android.systemui.dagger.SysUISingleton
30 import com.android.systemui.dagger.qualifiers.Application
31 import com.android.systemui.dagger.qualifiers.Background
32 import com.android.systemui.res.R
33 import com.android.systemui.screenshot.scroll.LongScreenshotActivity
34 import com.android.systemui.shared.Flags.usePreferredImageEditor
35 import java.util.function.Consumer
36 import javax.inject.Inject
37 import kotlinx.coroutines.CoroutineDispatcher
38 import kotlinx.coroutines.CoroutineScope
39 import kotlinx.coroutines.launch
40 import kotlinx.coroutines.withContext
41
42 @SysUISingleton
43 class ActionIntentCreator
44 @Inject
45 constructor(
46 private val context: Context,
47 private val packageManager: PackageManager,
48 @Application private val applicationScope: CoroutineScope,
49 @Background private val backgroundDispatcher: CoroutineDispatcher,
50 ) {
51 /** @return a chooser intent to share the given URI. */
createSharenull52 fun createShare(uri: Uri): Intent = createShare(uri, subject = null, text = null)
53
54 /** @return a chooser intent to share the given URI with the optional provided subject. */
55 fun createShareWithSubject(uri: Uri, subject: String): Intent =
56 createShare(uri, subject = subject)
57
58 /** @return a chooser intent to share the given URI with the optional provided extra text. */
59 fun createShareWithText(uri: Uri, extraText: String): Intent =
60 createShare(uri, text = extraText)
61
62 private fun createShare(rawUri: Uri, subject: String? = null, text: String? = null): Intent {
63 val uri = uriWithoutUserId(rawUri)
64
65 // Create a share intent, this will always go through the chooser activity first
66 // which should not trigger auto-enter PiP
67 val sharingIntent =
68 Intent(Intent.ACTION_SEND).apply {
69 setDataAndType(uri, "image/png")
70 putExtra(Intent.EXTRA_STREAM, uri)
71
72 // Include URI in ClipData also, so that grantPermission picks it up.
73 // We don't use setData here because some apps interpret this as "to:".
74 clipData =
75 ClipData(
76 ClipDescription("content", arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)),
77 ClipData.Item(uri),
78 )
79
80 subject?.let { putExtra(Intent.EXTRA_SUBJECT, subject) }
81 text?.let { putExtra(Intent.EXTRA_TEXT, text) }
82 addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
83 addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
84 }
85
86 return Intent.createChooser(sharingIntent, null)
87 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
88 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
89 .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
90 }
91
92 // Non-suspend version for java compat
createEditnull93 fun createEdit(rawUri: Uri, consumer: Consumer<Intent>) {
94 applicationScope.launch { consumer.accept(createEdit(rawUri)) }
95 }
96
97 /**
98 * @return an ACTION_EDIT intent for the given URI, directed to config_preferredScreenshotEditor
99 * if enabled, falling back to config_screenshotEditor if that's non-empty.
100 */
createEditnull101 suspend fun createEdit(rawUri: Uri): Intent {
102 val uri = uriWithoutUserId(rawUri)
103 val editIntent = Intent(Intent.ACTION_EDIT)
104
105 if (usePreferredImageEditor()) {
106 // Use the preferred editor if it's available, otherwise fall back to the default editor
107 editIntent.component = preferredEditor() ?: defaultEditor()
108 } else {
109 val editor = context.getString(R.string.config_screenshotEditor)
110 if (editor.isNotEmpty()) {
111 editIntent.component = ComponentName.unflattenFromString(editor)
112 }
113 }
114
115 return editIntent
116 .setDataAndType(uri, "image/png")
117 .putExtra(EXTRA_EDIT_SOURCE, EDIT_SOURCE_SCREENSHOT)
118 .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
119 .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
120 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
121 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
122 }
123
124 /** @return an Intent to start the LongScreenshotActivity */
createLongScreenshotIntentnull125 fun createLongScreenshotIntent(owner: UserHandle): Intent {
126 return Intent(context, LongScreenshotActivity::class.java)
127 .putExtra(LongScreenshotActivity.EXTRA_SCREENSHOT_USER_HANDLE, owner)
128 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
129 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
130 .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
131 }
132
preferredEditornull133 private suspend fun preferredEditor(): ComponentName? =
134 runCatching {
135 val preferredEditor = context.getString(R.string.config_preferredScreenshotEditor)
136 val component = ComponentName.unflattenFromString(preferredEditor) ?: return null
137
138 return if (isComponentAvailable(component)) component else null
139 }
140 .getOrNull()
141
isComponentAvailablenull142 private suspend fun isComponentAvailable(component: ComponentName): Boolean =
143 withContext(backgroundDispatcher) {
144 try {
145 val info =
146 packageManager.getPackageInfo(
147 component.packageName,
148 PackageManager.GET_ACTIVITIES,
149 )
150 info.activities?.firstOrNull {
151 it.componentName.className == component.className
152 } != null
153 } catch (e: NameNotFoundException) {
154 false
155 }
156 }
157
defaultEditornull158 private fun defaultEditor(): ComponentName? =
159 runCatching {
160 context.getString(R.string.config_screenshotEditor).let {
161 ComponentName.unflattenFromString(it)
162 }
163 }
164 .getOrNull()
165
166 companion object {
167 private const val EXTRA_EDIT_SOURCE = "edit_source"
168 private const val EDIT_SOURCE_SCREENSHOT = "screenshot"
169 }
170 }
171
172 /**
173 * URIs here are passed only via Intent which are sent to the target user via Intent. Because of
174 * this, the userId component can be removed to prevent compatibility issues when an app attempts
175 * valid a URI containing a userId within the authority.
176 */
uriWithoutUserIdnull177 private fun uriWithoutUserId(uri: Uri): Uri {
178 return ContentProvider.getUriWithoutUserId(uri)
179 }
180