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