• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.getKey());
116 
117         final BindEntry bindEntry = getBindEntry(entry);
118         if (bindEntry == null) {
119             return;
120         }
121         bindEntry.row = row;
122         if (bindEntry.invalidated) {
123             requestPipelineRun(entry);
124         }
125     }
126 
onBindRequested( @onNull NotificationEntry entry, @NonNull CancellationSignal signal, @Nullable BindCallback callback)127     private void onBindRequested(
128             @NonNull NotificationEntry entry,
129             @NonNull CancellationSignal signal,
130             @Nullable BindCallback callback) {
131         final BindEntry bindEntry = getBindEntry(entry);
132         if (bindEntry == null) {
133             // Invalidating views for a notification that is not active.
134             return;
135         }
136 
137         bindEntry.invalidated = true;
138 
139         // Put in new callback.
140         if (callback != null) {
141             final Set<BindCallback> callbacks = bindEntry.callbacks;
142             callbacks.add(callback);
143             signal.setOnCancelListener(() -> callbacks.remove(callback));
144         }
145 
146         requestPipelineRun(entry);
147     }
148 
149     /**
150      * Request pipeline to start.
151      *
152      * We avoid starting the pipeline immediately as multiple clients may request rebinds
153      * back-to-back due to a single change (e.g. notification update), and it's better to start
154      * the real work once rather than repeatedly start and cancel it.
155      */
requestPipelineRun(NotificationEntry entry)156     private void requestPipelineRun(NotificationEntry entry) {
157         mLogger.logRequestPipelineRun(entry.getKey());
158 
159         final BindEntry bindEntry = getBindEntry(entry);
160         if (bindEntry.row == null) {
161             // Row is not managed yet but may be soon. Stop for now.
162             mLogger.logRequestPipelineRowNotSet(entry.getKey());
163             return;
164         }
165 
166         // Abort any existing pipeline run
167         mStage.abortStage(entry, bindEntry.row);
168 
169         if (!mMainHandler.hasMessages(START_PIPELINE_MSG, entry)) {
170             Message msg = Message.obtain(mMainHandler, START_PIPELINE_MSG, entry);
171             mMainHandler.sendMessage(msg);
172         }
173     }
174 
175     /**
176      * Run the pipeline for the notification, ensuring all views are bound when finished. Call all
177      * callbacks when the run finishes. If a run is already in progress, it is restarted.
178      */
startPipeline(NotificationEntry entry)179     private void startPipeline(NotificationEntry entry) {
180         mLogger.logStartPipeline(entry.getKey());
181 
182         if (mStage == null) {
183             throw new IllegalStateException("No stage was ever set on the pipeline");
184         }
185 
186         final BindEntry bindEntry = mBindEntries.get(entry);
187         final ExpandableNotificationRow row = bindEntry.row;
188 
189         mStage.executeStage(entry, row, (en) -> onPipelineComplete(en));
190     }
191 
onPipelineComplete(NotificationEntry entry)192     private void onPipelineComplete(NotificationEntry entry) {
193         final BindEntry bindEntry = getBindEntry(entry);
194         final Set<BindCallback> callbacks = bindEntry.callbacks;
195 
196         mLogger.logFinishedPipeline(entry.getKey(), callbacks.size());
197 
198         bindEntry.invalidated = false;
199         // Move all callbacks to separate list as callbacks may themselves add/remove callbacks.
200         // TODO: Throw an exception for this re-entrant behavior once we deprecate
201         // NotificationGroupAlertTransferHelper
202         mScratchCallbacksList.addAll(callbacks);
203         callbacks.clear();
204         for (int i = 0; i < mScratchCallbacksList.size(); i++) {
205             mScratchCallbacksList.get(i).onBindFinished(entry);
206         }
207         mScratchCallbacksList.clear();
208     }
209 
210     private final NotifCollectionListener mCollectionListener = new NotifCollectionListener() {
211         @Override
212         public void onEntryInit(NotificationEntry entry) {
213             mBindEntries.put(entry, new BindEntry());
214             mStage.createStageParams(entry);
215         }
216 
217         @Override
218         public void onEntryCleanUp(NotificationEntry entry) {
219             BindEntry bindEntry = mBindEntries.remove(entry);
220             ExpandableNotificationRow row = bindEntry.row;
221             if (row != null) {
222                 mStage.abortStage(entry, row);
223             }
224             mStage.deleteStageParams(entry);
225             mMainHandler.removeMessages(START_PIPELINE_MSG, entry);
226         }
227     };
228 
getBindEntry(NotificationEntry entry)229     private @NonNull BindEntry getBindEntry(NotificationEntry entry) {
230         final BindEntry bindEntry = mBindEntries.get(entry);
231         return bindEntry;
232     }
233 
234     /**
235      * Interface for bind callback.
236      */
237     public interface BindCallback {
238         /**
239          * Called when all views are fully bound on the notification.
240          */
onBindFinished(NotificationEntry entry)241         void onBindFinished(NotificationEntry entry);
242     }
243 
244     private class BindEntry {
245         public ExpandableNotificationRow row;
246         public final Set<BindCallback> callbacks = new ArraySet<>();
247         public boolean invalidated;
248     }
249 
250     private static final int START_PIPELINE_MSG = 1;
251 
252     private class NotifBindPipelineHandler extends Handler {
253 
NotifBindPipelineHandler(Looper looper)254         NotifBindPipelineHandler(Looper looper) {
255             super(looper);
256         }
257 
258         @Override
handleMessage(Message msg)259         public void handleMessage(Message msg) {
260             switch (msg.what) {
261                 case START_PIPELINE_MSG:
262                     NotificationEntry entry = (NotificationEntry) msg.obj;
263                     startPipeline(entry);
264                     break;
265                 default:
266                     throw new IllegalArgumentException("Unknown message type: " + msg.what);
267             }
268         }
269     }
270 }
271