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