1 /*
2  * Copyright (C) 2020 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.row;
18 
19 import android.util.ArrayMap;
20 import android.util.ArraySet;
21 import android.widget.FrameLayout;
22 
23 import androidx.annotation.MainThread;
24 import androidx.annotation.NonNull;
25 import androidx.annotation.Nullable;
26 import androidx.core.os.CancellationSignal;
27 
28 import com.android.systemui.dagger.SysUISingleton;
29 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
30 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
31 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
32 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
33 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
34 
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 
40 import javax.inject.Inject;
41 
42 /**
43  * {@link NotifBindPipeline} is responsible for converting notifications from their data form to
44  * their actual inflated views. It is essentially a control class that composes notification view
45  * binding logic (i.e. {@link BindStage}) in response to explicit bind requests. At the end of the
46  * pipeline, the notification's bound views are guaranteed to be correct and up-to-date, and any
47  * registered callbacks will be called.
48  *
49  * The pipeline ensures that a notification's top-level view and its content views are bound.
50  * Currently, a notification's top-level view, the {@link ExpandableNotificationRow} is essentially
51  * just a {@link FrameLayout} for various different content views that are switched in and out as
52  * appropriate. These include a contracted view, expanded view, heads up view, and sensitive view on
53  * keyguard. See {@link InflationFlag}. These content views themselves can have child views added
54  * on depending on different factors. For example, notification actions and smart replies are views
55  * that are dynamically added to these content views after they're inflated. Finally, aside from
56  * the app provided content views, System UI itself also provides some content views that are shown
57  * occasionally (e.g. {@link NotificationGuts}). Many of these are business logic specific views
58  * and the requirements surrounding them may change over time, so the pipeline must handle
59  * composing the logic as necessary.
60  *
61  * Note that bind requests do not only occur from add/updates from updates from the app. For
62  * example, the user may make changes to device settings (e.g. sensitive notifications on lock
63  * screen) or we may want to make certain optimizations for the sake of memory or performance (e.g
64  * freeing views when not visible). Oftentimes, we also need to wait for these changes to complete
65  * before doing something else (e.g. moving a notification to the top of the screen to heads up).
66  * The pipeline thus handles bind requests from across the system and provides a way for
67  * requesters to know when the change is propagated to the view.
68  *
69  * Right now, we only support one attached {@link BindStage} which just does all the binding but we
70  * should eventually support multiple stages once content inflation is made more modular.
71  * In particular, row inflation/binding, which is handled by {@link NotificationRowBinder} should
72  * probably be moved here in the future as a stage. Right now, the pipeline just manages content
73  * views and assumes that a row is given to it when it's inflated.
74  */
75 @MainThread
76 @SysUISingleton
77 public final class NotifBindPipeline {
78     private final Map<NotificationEntry, BindEntry> mBindEntries = new ArrayMap<>();
79     private final NotifBindPipelineLogger mLogger;
80     private final List<BindCallback> mScratchCallbacksList = new ArrayList<>();
81     private final Processor<NotificationEntry> mStartProcessor;
82     private BindStage mStage;
83 
84     @Inject
NotifBindPipeline( CommonNotifCollection collection, NotifBindPipelineLogger logger, NotificationEntryProcessorFactory processorFactory)85     NotifBindPipeline(
86             CommonNotifCollection collection,
87             NotifBindPipelineLogger logger,
88             NotificationEntryProcessorFactory processorFactory) {
89         collection.addCollectionListener(mCollectionListener);
90         mLogger = logger;
91         mStartProcessor = processorFactory.create(this::startPipeline);
92     }
93 
94     /**
95      * Set the bind stage for binding notification row content.
96      */
setStage( BindStage stage)97     public void setStage(
98             BindStage stage) {
99         mLogger.logStageSet(stage.getClass().getName());
100 
101         mStage = stage;
102         mStage.setBindRequestListener(this::onBindRequested);
103     }
104 
105     /**
106      * Start managing the row's content for a given notification.
107      */
manageRow( @onNull NotificationEntry entry, @NonNull ExpandableNotificationRow row)108     public void manageRow(
109             @NonNull NotificationEntry entry,
110             @NonNull ExpandableNotificationRow row) {
111         mLogger.logManagedRow(entry);
112         mLogger.logManagedRow(entry);
113 
114         final BindEntry bindEntry = getBindEntry(entry);
115         if (bindEntry == null) {
116             return;
117         }
118         bindEntry.row = row;
119         if (bindEntry.invalidated) {
120             requestPipelineRun(entry);
121         }
122     }
123 
onBindRequested( @onNull NotificationEntry entry, @NonNull CancellationSignal signal, @Nullable BindCallback callback)124     private void onBindRequested(
125             @NonNull NotificationEntry entry,
126             @NonNull CancellationSignal signal,
127             @Nullable BindCallback callback) {
128         final BindEntry bindEntry = getBindEntry(entry);
129         if (bindEntry == null) {
130             // Invalidating views for a notification that is not active.
131             return;
132         }
133 
134         bindEntry.invalidated = true;
135 
136         // Put in new callback.
137         if (callback != null) {
138             final Set<BindCallback> callbacks = bindEntry.callbacks;
139             callbacks.add(callback);
140             signal.setOnCancelListener(() -> callbacks.remove(callback));
141         }
142 
143         requestPipelineRun(entry);
144     }
145 
146     /**
147      * Request pipeline to start.
148      *
149      * We avoid starting the pipeline immediately as multiple clients may request rebinds
150      * back-to-back due to a single change (e.g. notification update), and it's better to start
151      * the real work once rather than repeatedly start and cancel it.
152      */
requestPipelineRun(NotificationEntry entry)153     private void requestPipelineRun(NotificationEntry entry) {
154         mLogger.logRequestPipelineRun(entry);
155 
156         final BindEntry bindEntry = getBindEntry(entry);
157         if (bindEntry.row == null) {
158             // Row is not managed yet but may be soon. Stop for now.
159             mLogger.logRequestPipelineRowNotSet(entry);
160             return;
161         }
162 
163         // Abort any existing pipeline run
164         mStage.abortStage(entry, bindEntry.row);
165 
166         mStartProcessor.request(entry);
167     }
168 
169     /**
170      * Run the pipeline for the notification, ensuring all views are bound when finished. Call all
171      * callbacks when the run finishes. If a run is already in progress, it is restarted.
172      */
startPipeline(NotificationEntry entry)173     private void startPipeline(NotificationEntry entry) {
174         mLogger.logStartPipeline(entry);
175 
176         if (mStage == null) {
177             throw new IllegalStateException("No stage was ever set on the pipeline");
178         }
179 
180         final BindEntry bindEntry = mBindEntries.get(entry);
181         final ExpandableNotificationRow row = bindEntry.row;
182 
183         mStage.executeStage(entry, row, (en) -> onPipelineComplete(en));
184     }
185 
onPipelineComplete(NotificationEntry entry)186     private void onPipelineComplete(NotificationEntry entry) {
187         final BindEntry bindEntry = getBindEntry(entry);
188         final Set<BindCallback> callbacks = bindEntry.callbacks;
189 
190         mLogger.logFinishedPipeline(entry, callbacks.size());
191 
192         bindEntry.invalidated = false;
193         // Move all callbacks to separate list as callbacks may themselves add/remove callbacks.
194         // TODO: Throw an exception for this re-entrant behavior once we deprecate
195         // NotificationGroupAlertTransferHelper
196         mScratchCallbacksList.addAll(callbacks);
197         callbacks.clear();
198         for (int i = 0; i < mScratchCallbacksList.size(); i++) {
199             mScratchCallbacksList.get(i).onBindFinished(entry);
200         }
201         mScratchCallbacksList.clear();
202     }
203 
204     private final NotifCollectionListener mCollectionListener = new NotifCollectionListener() {
205         @Override
206         public void onEntryInit(NotificationEntry entry) {
207             mBindEntries.put(entry, new BindEntry());
208             mStage.createStageParams(entry);
209         }
210 
211         @Override
212         public void onEntryCleanUp(NotificationEntry entry) {
213             BindEntry bindEntry = mBindEntries.remove(entry);
214             ExpandableNotificationRow row = bindEntry.row;
215             if (row != null) {
216                 mStage.abortStage(entry, row);
217             }
218             mStage.deleteStageParams(entry);
219             mStartProcessor.cancel(entry);
220         }
221     };
222 
getBindEntry(NotificationEntry entry)223     private @NonNull BindEntry getBindEntry(NotificationEntry entry) {
224         final BindEntry bindEntry = mBindEntries.get(entry);
225         return bindEntry;
226     }
227 
228     /**
229      * Interface for bind callback.
230      */
231     public interface BindCallback {
232         /**
233          * Called when all views are fully bound on the notification.
234          */
onBindFinished(NotificationEntry entry)235         void onBindFinished(NotificationEntry entry);
236     }
237 
238     private class BindEntry {
239         public ExpandableNotificationRow row;
240         public final Set<BindCallback> callbacks = new ArraySet<>();
241         public boolean invalidated;
242     }
243 }
244