1 /*
<lambda>null2  * Copyright (C) 2021 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.statusbar.policy
18 
19 import android.app.ActivityOptions
20 import android.app.Notification
21 import android.app.PendingIntent
22 import android.app.RemoteInput
23 import android.content.Intent
24 import android.content.pm.ShortcutManager
25 import android.net.Uri
26 import android.os.Bundle
27 import android.os.SystemClock
28 import android.text.TextUtils
29 import android.util.ArraySet
30 import android.util.Log
31 import android.view.View
32 import com.android.internal.logging.UiEventLogger
33 import com.android.systemui.flags.FeatureFlags
34 import com.android.systemui.res.R
35 import com.android.systemui.statusbar.NotificationRemoteInputManager
36 import com.android.systemui.statusbar.RemoteInputController
37 import com.android.systemui.statusbar.notification.collection.NotificationEntry
38 import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo
39 import com.android.systemui.statusbar.policy.RemoteInputView.NotificationRemoteInputEvent
40 import com.android.systemui.statusbar.policy.dagger.RemoteInputViewScope
41 import javax.inject.Inject
42 
43 interface RemoteInputViewController {
44     fun bind()
45     fun unbind()
46 
47     val isActive: Boolean
48 
49     /**
50      * A [NotificationRemoteInputManager.BouncerChecker] that will be used to determine if the
51      * device needs to be unlocked before sending the RemoteInput.
52      */
53     var bouncerChecker: NotificationRemoteInputManager.BouncerChecker?
54 
55     // TODO(b/193539698): these properties probably shouldn't be nullable
56     /** A [PendingIntent] to be used to send the RemoteInput. */
57     var pendingIntent: PendingIntent?
58     /** The [RemoteInput] data backing this Controller. */
59     var remoteInput: RemoteInput?
60     /** Other [RemoteInput]s from the notification associated with this Controller. */
61     var remoteInputs: Array<RemoteInput>?
62 
63     /**
64      * Sets the smart reply that should be inserted in the remote input, or `null` if the user is
65      * not editing a smart reply.
66      */
67     fun setEditedSuggestionInfo(info: EditedSuggestionInfo?)
68 
69     /**
70      * Tries to find an action in {@param actions} that matches the current pending intent
71      * of this view and updates its state to that of the found action
72      *
73      * @return true if a matching action was found, false otherwise
74      */
75     fun updatePendingIntentFromActions(actions: Array<Notification.Action>?): Boolean
76 
77     /** Registers a listener for send events. */
78     fun addOnSendRemoteInputListener(listener: OnSendRemoteInputListener)
79 
80     /** Unregisters a listener previously registered via [addOnSendRemoteInputListener] */
81     fun removeOnSendRemoteInputListener(listener: OnSendRemoteInputListener)
82 
83     fun close()
84 
85     fun focus()
86 
87     fun stealFocusFrom(other: RemoteInputViewController) {
88         other.close()
89         remoteInput = other.remoteInput
90         remoteInputs = other.remoteInputs
91         pendingIntent = other.pendingIntent
92         focus()
93     }
94 }
95 
96 /** Listener for send events  */
97 interface OnSendRemoteInputListener {
98 
99     /** Invoked when the remote input has been sent successfully.  */
onSendRemoteInputnull100     fun onSendRemoteInput()
101 
102     /**
103      * Invoked when the user had requested to send the remote input, but authentication was
104      * required and the bouncer was shown instead.
105      */
106     fun onSendRequestBounced()
107 }
108 
109 private const val TAG = "RemoteInput"
110 
111 @RemoteInputViewScope
112 class RemoteInputViewControllerImpl @Inject constructor(
113     private val view: RemoteInputView,
114     private val entry: NotificationEntry,
115     private val remoteInputQuickSettingsDisabler: RemoteInputQuickSettingsDisabler,
116     private val remoteInputController: RemoteInputController,
117     private val shortcutManager: ShortcutManager,
118     private val uiEventLogger: UiEventLogger,
119     private val mFlags: FeatureFlags
120 ) : RemoteInputViewController {
121 
122     private val onSendListeners = ArraySet<OnSendRemoteInputListener>()
123     private val resources get() = view.resources
124 
125     private var isBound = false
126 
127     override var bouncerChecker: NotificationRemoteInputManager.BouncerChecker? = null
128 
129     override var remoteInput: RemoteInput? = null
130         set(value) {
131             field = value
132             value?.takeIf { isBound }?.let {
133                 view.setHintText(it.label)
134                 view.setSupportedMimeTypes(it.allowedDataTypes)
135             }
136         }
137 
138     override var pendingIntent: PendingIntent? = null
139     override var remoteInputs: Array<RemoteInput>? = null
140 
141     override val isActive: Boolean get() = view.isActive
142 
143     override fun bind() {
144         if (isBound) return
145         isBound = true
146 
147         // TODO: refreshUI method?
148         remoteInput?.let {
149             view.setHintText(it.label)
150             view.setSupportedMimeTypes(it.allowedDataTypes)
151         }
152 
153         view.addOnEditTextFocusChangedListener(onFocusChangeListener)
154         view.addOnSendRemoteInputListener(onSendRemoteInputListener)
155     }
156 
157     override fun unbind() {
158         if (!isBound) return
159         isBound = false
160 
161         view.removeOnEditTextFocusChangedListener(onFocusChangeListener)
162         view.removeOnSendRemoteInputListener(onSendRemoteInputListener)
163     }
164 
165     override fun setEditedSuggestionInfo(info: EditedSuggestionInfo?) {
166         entry.editedSuggestionInfo = info
167         if (info != null) {
168             entry.remoteInputText = info.originalText
169             entry.remoteInputAttachment = null
170         }
171     }
172 
173     override fun updatePendingIntentFromActions(actions: Array<Notification.Action>?): Boolean {
174         actions ?: return false
175         val current: Intent = pendingIntent?.intent ?: return false
176         for (a in actions) {
177             val actionIntent = a.actionIntent ?: continue
178             val inputs = a.remoteInputs ?: continue
179             if (!current.filterEquals(actionIntent.intent)) continue
180             val input = inputs.firstOrNull { it.allowFreeFormInput } ?: continue
181             pendingIntent = actionIntent
182             remoteInput = input
183             remoteInputs = inputs
184             setEditedSuggestionInfo(null)
185             return true
186         }
187         return false
188     }
189 
190     override fun addOnSendRemoteInputListener(listener: OnSendRemoteInputListener) {
191         onSendListeners.add(listener)
192     }
193 
194     /** Removes a previously-added listener for send events on this RemoteInputView  */
195     override fun removeOnSendRemoteInputListener(listener: OnSendRemoteInputListener) {
196         onSendListeners.remove(listener)
197     }
198 
199     override fun close() {
200         view.close()
201     }
202 
203     override fun focus() {
204         view.focus()
205     }
206 
207     private val onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
208         remoteInputQuickSettingsDisabler.setRemoteInputActive(hasFocus)
209     }
210 
211     private val onSendRemoteInputListener = Runnable {
212         val remoteInput = remoteInput ?: run {
213             Log.e(TAG, "cannot send remote input, RemoteInput data is null")
214             return@Runnable
215         }
216         val pendingIntent = pendingIntent ?: run {
217             Log.e(TAG, "cannot send remote input, PendingIntent is null")
218             return@Runnable
219         }
220         val intent = prepareRemoteInput(remoteInput)
221         sendRemoteInput(pendingIntent, intent)
222     }
223 
224     private fun sendRemoteInput(pendingIntent: PendingIntent, intent: Intent) {
225         if (bouncerChecker?.showBouncerIfNecessary() == true) {
226             view.hideIme()
227             for (listener in onSendListeners.toList()) {
228                 listener.onSendRequestBounced()
229             }
230             return
231         }
232 
233         view.startSending()
234 
235         entry.lastRemoteInputSent = SystemClock.elapsedRealtime()
236         entry.mRemoteEditImeAnimatingAway = true
237         remoteInputController.addSpinning(entry.key, view.mToken)
238         remoteInputController.removeRemoteInput(entry, view.mToken,
239                /* reason= */ "RemoteInputViewController#sendRemoteInput")
240         remoteInputController.remoteInputSent(entry)
241         entry.setHasSentReply()
242 
243         for (listener in onSendListeners.toList()) {
244             listener.onSendRemoteInput()
245         }
246 
247         // Tell ShortcutManager that this package has been "activated". ShortcutManager will reset
248         // the throttling for this package.
249         // Strictly speaking, the intent receiver may be different from the notification publisher,
250         // but that's an edge case, and also because we can't always know which package will receive
251         // an intent, so we just reset for the publisher.
252         shortcutManager.onApplicationActive(entry.sbn.packageName, entry.sbn.user.identifier)
253 
254         uiEventLogger.logWithInstanceId(
255                 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_SEND,
256                 entry.sbn.uid, entry.sbn.packageName,
257                 entry.sbn.instanceId)
258 
259         try {
260             val options = ActivityOptions.makeBasic()
261             options.setPendingIntentBackgroundActivityStartMode(
262                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
263             pendingIntent.send(view.context, 0, intent, null, null, null, options.toBundle())
264         } catch (e: PendingIntent.CanceledException) {
265             Log.i(TAG, "Unable to send remote input result", e)
266             uiEventLogger.logWithInstanceId(
267                     NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_FAILURE,
268                     entry.sbn.uid, entry.sbn.packageName,
269                     entry.sbn.instanceId)
270         }
271 
272         view.clearAttachment()
273     }
274 
275     /**
276      * Reply intent
277      * @return returns intent with granted URI permissions that should be used immediately
278      */
279     private fun prepareRemoteInput(remoteInput: RemoteInput): Intent =
280         if (entry.remoteInputAttachment == null)
281             prepareRemoteInputFromText(remoteInput)
282         else prepareRemoteInputFromData(
283                 remoteInput,
284                 entry.remoteInputMimeType,
285                 entry.remoteInputUri)
286 
287     private fun prepareRemoteInputFromText(remoteInput: RemoteInput): Intent {
288         val results = Bundle()
289         results.putString(remoteInput.resultKey, view.text.toString())
290         val fillInIntent = Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
291         RemoteInput.addResultsToIntent(remoteInputs, fillInIntent, results)
292         entry.remoteInputText = view.text
293         view.clearAttachment()
294         entry.remoteInputUri = null
295         entry.remoteInputMimeType = null
296         RemoteInput.setResultsSource(fillInIntent, remoteInputResultsSource)
297         return fillInIntent
298     }
299 
300     private fun prepareRemoteInputFromData(
301         remoteInput: RemoteInput,
302         contentType: String,
303         data: Uri
304     ): Intent {
305         val results = HashMap<String, Uri>()
306         results[contentType] = data
307         // grant for the target app.
308         remoteInputController.grantInlineReplyUriPermission(entry.sbn, data)
309         val fillInIntent = Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
310         RemoteInput.addDataResultToIntent(remoteInput, fillInIntent, results)
311         val bundle = Bundle()
312         bundle.putString(remoteInput.resultKey, view.text.toString())
313         RemoteInput.addResultsToIntent(remoteInputs, fillInIntent, bundle)
314         val attachmentText: CharSequence = entry.remoteInputAttachment.clip.description.label
315         val attachmentLabel =
316                 if (TextUtils.isEmpty(attachmentText))
317                     resources.getString(R.string.remote_input_image_insertion_text)
318                 else attachmentText
319         // add content description to reply text for context
320         val fullText =
321                 if (TextUtils.isEmpty(view.text)) attachmentLabel
322                 else "\"" + attachmentLabel + "\" " + view.text
323         entry.remoteInputText = fullText
324 
325         // mirror prepareRemoteInputFromText for text input
326         RemoteInput.setResultsSource(fillInIntent, remoteInputResultsSource)
327         return fillInIntent
328     }
329 
330     private val remoteInputResultsSource
331         get() = entry.editedSuggestionInfo
332                 ?.let { RemoteInput.SOURCE_CHOICE }
333                 ?: RemoteInput.SOURCE_FREE_FORM_INPUT
334 }
335