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