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 }