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