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.notification.collection.coordinator 18 19 import android.os.Handler 20 import android.service.notification.NotificationListenerService.REASON_CANCEL 21 import android.service.notification.NotificationListenerService.REASON_CLICK 22 import android.util.Log 23 import androidx.annotation.VisibleForTesting 24 import com.android.systemui.Dumpable 25 import com.android.systemui.dagger.qualifiers.Main 26 import com.android.systemui.dump.DumpManager 27 import com.android.systemui.statusbar.NotificationRemoteInputManager 28 import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputListener 29 import com.android.systemui.statusbar.RemoteInputController 30 import com.android.systemui.statusbar.RemoteInputNotificationRebuilder 31 import com.android.systemui.statusbar.SmartReplyController 32 import com.android.systemui.statusbar.notification.collection.NotifPipeline 33 import com.android.systemui.statusbar.notification.collection.NotificationEntry 34 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope 35 import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater 36 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener 37 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender 38 import com.android.systemui.statusbar.notification.collection.notifcollection.SelfTrackingLifetimeExtender 39 import java.io.PrintWriter 40 import javax.inject.Inject 41 42 private const val TAG = "RemoteInputCoordinator" 43 44 /** 45 * How long to wait before auto-dismissing a notification that was kept for active remote input, and 46 * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel 47 * these given that they technically don't exist anymore. We wait a bit in case the app issues 48 * an update, and to also give the other lifetime extenders a beat to decide they want it. 49 */ 50 private const val REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY: Long = 500 51 52 /** 53 * How long to wait before releasing a lifetime extension when requested to do so due to a user 54 * interaction (such as tapping another action). 55 * We wait a bit in case the app issues an update in response to the action, but not too long or we 56 * risk appearing unresponsive to the user. 57 */ 58 private const val REMOTE_INPUT_EXTENDER_RELEASE_DELAY: Long = 200 59 60 /** Whether this class should print spammy debug logs */ 61 private val DEBUG: Boolean by lazy { Log.isLoggable(TAG, Log.DEBUG) } 62 63 @CoordinatorScope 64 class RemoteInputCoordinator @Inject constructor( 65 dumpManager: DumpManager, 66 private val mRebuilder: RemoteInputNotificationRebuilder, 67 private val mNotificationRemoteInputManager: NotificationRemoteInputManager, 68 @Main private val mMainHandler: Handler, 69 private val mSmartReplyController: SmartReplyController 70 ) : Coordinator, RemoteInputListener, Dumpable { 71 72 @VisibleForTesting val mRemoteInputHistoryExtender = RemoteInputHistoryExtender() 73 @VisibleForTesting val mSmartReplyHistoryExtender = SmartReplyHistoryExtender() 74 @VisibleForTesting val mRemoteInputActiveExtender = RemoteInputActiveExtender() 75 private val mRemoteInputLifetimeExtenders = listOf( 76 mRemoteInputHistoryExtender, 77 mSmartReplyHistoryExtender, 78 mRemoteInputActiveExtender 79 ) 80 81 private lateinit var mNotifUpdater: InternalNotifUpdater 82 83 init { 84 dumpManager.registerDumpable(this) 85 } 86 87 fun getLifetimeExtenders(): List<NotifLifetimeExtender> = mRemoteInputLifetimeExtenders 88 89 override fun attach(pipeline: NotifPipeline) { 90 mNotificationRemoteInputManager.setRemoteInputListener(this) 91 mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) } 92 mNotifUpdater = pipeline.getInternalNotifUpdater(TAG) 93 pipeline.addCollectionListener(mCollectionListener) 94 } 95 96 val mCollectionListener = object : NotifCollectionListener { 97 override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) { 98 if (DEBUG) { 99 Log.d(TAG, "mCollectionListener.onEntryUpdated(entry=${entry.key}," + 100 " fromSystem=$fromSystem)") 101 } 102 if (fromSystem) { 103 // Mark smart replies as sent whenever a notification is updated by the app, 104 // otherwise the smart replies are never marked as sent. 105 mSmartReplyController.stopSending(entry) 106 } 107 } 108 109 override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { 110 if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})") 111 // We're removing the notification, the smart reply controller can forget about it. 112 // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear it. 113 mSmartReplyController.stopSending(entry) 114 115 // When we know the entry will not be lifetime extended, clean up the remote input view 116 // TODO: Share code with NotifCollection.cannotBeLifetimeExtended 117 if (reason == REASON_CANCEL || reason == REASON_CLICK) { 118 mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry) 119 } 120 } 121 } 122 123 override fun dump(pw: PrintWriter, args: Array<out String>) { 124 mRemoteInputLifetimeExtenders.forEach { it.dump(pw, args) } 125 } 126 127 override fun onRemoteInputSent(entry: NotificationEntry) { 128 if (DEBUG) Log.d(TAG, "onRemoteInputSent(entry=${entry.key})") 129 // These calls effectively ensure the freshness of the lifetime extensions. 130 // NOTE: This is some trickery! By removing the lifetime extensions when we know they should 131 // be immediately re-upped, we ensure that the side-effects of the lifetime extenders get to 132 // fire again, thus ensuring that we add subsequent replies to the notification. 133 mRemoteInputHistoryExtender.endLifetimeExtension(entry.key) 134 mSmartReplyHistoryExtender.endLifetimeExtension(entry.key) 135 136 // If we're extending for remote input being active, then from the apps point of 137 // view it is already canceled, so we'll need to cancel it on the apps behalf 138 // now that a reply has been sent. However, delay so that the app has time to posts an 139 // update in the mean time, and to give another lifetime extender time to pick it up. 140 mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key, 141 REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY) 142 } 143 144 private fun onSmartReplySent(entry: NotificationEntry, reply: CharSequence) { 145 if (DEBUG) Log.d(TAG, "onSmartReplySent(entry=${entry.key})") 146 val newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply) 147 mNotifUpdater.onInternalNotificationUpdate(newSbn, 148 "Adding smart reply spinner for sent") 149 150 // If we're extending for remote input being active, then from the apps point of 151 // view it is already canceled, so we'll need to cancel it on the apps behalf 152 // now that a reply has been sent. However, delay so that the app has time to posts an 153 // update in the mean time, and to give another lifetime extender time to pick it up. 154 mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key, 155 REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY) 156 } 157 158 override fun onPanelCollapsed() { 159 mRemoteInputActiveExtender.endAllLifetimeExtensions() 160 } 161 162 override fun isNotificationKeptForRemoteInputHistory(key: String) = 163 mRemoteInputHistoryExtender.isExtending(key) || 164 mSmartReplyHistoryExtender.isExtending(key) 165 166 override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) { 167 if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})") 168 mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key, 169 REMOTE_INPUT_EXTENDER_RELEASE_DELAY) 170 mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key, 171 REMOTE_INPUT_EXTENDER_RELEASE_DELAY) 172 mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key, 173 REMOTE_INPUT_EXTENDER_RELEASE_DELAY) 174 } 175 176 override fun setRemoteInputController(remoteInputController: RemoteInputController) { 177 mSmartReplyController.setCallback(this::onSmartReplySent) 178 } 179 180 @VisibleForTesting 181 inner class RemoteInputHistoryExtender : 182 SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) { 183 184 override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean = 185 mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry) 186 187 override fun onStartedLifetimeExtension(entry: NotificationEntry) { 188 val newSbn = mRebuilder.rebuildForRemoteInputReply(entry) 189 entry.onRemoteInputInserted() 190 mNotifUpdater.onInternalNotificationUpdate(newSbn, 191 "Extending lifetime of notification with remote input") 192 // TODO: Check if the entry was removed due perhaps to an inflation exception? 193 } 194 } 195 196 @VisibleForTesting 197 inner class SmartReplyHistoryExtender : 198 SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) { 199 200 override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean = 201 mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry) 202 203 override fun onStartedLifetimeExtension(entry: NotificationEntry) { 204 val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry) 205 mSmartReplyController.stopSending(entry) 206 mNotifUpdater.onInternalNotificationUpdate(newSbn, 207 "Extending lifetime of notification with smart reply") 208 // TODO: Check if the entry was removed due perhaps to an inflation exception? 209 } 210 211 override fun onCanceledLifetimeExtension(entry: NotificationEntry) { 212 // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear it. 213 mSmartReplyController.stopSending(entry) 214 } 215 } 216 217 @VisibleForTesting 218 inner class RemoteInputActiveExtender : 219 SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) { 220 221 override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean = 222 mNotificationRemoteInputManager.isRemoteInputActive(entry) 223 } 224 }