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 }