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