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