• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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