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