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 */
<lambda>null61 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
getLifetimeExtendersnull87 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 {
onEntryUpdatednull97 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
onEntryRemovednull109 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
dumpnull123 override fun dump(pw: PrintWriter, args: Array<out String>) {
124 mRemoteInputLifetimeExtenders.forEach { it.dump(pw, args) }
125 }
126
onRemoteInputSentnull127 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
onSmartReplySentnull144 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
onPanelCollapsednull158 override fun onPanelCollapsed() {
159 mRemoteInputActiveExtender.endAllLifetimeExtensions()
160 }
161
isNotificationKeptForRemoteInputHistorynull162 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
setRemoteInputControllernull176 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
queryShouldExtendLifetimenull184 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
queryShouldExtendLifetimenull200 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
onCanceledLifetimeExtensionnull211 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
queryShouldExtendLifetimenull221 override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
222 mNotificationRemoteInputManager.isRemoteInputActive(entry)
223 }
224 }