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