summaryrefslogtreecommitdiff
path: root/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java
blob: ea564ddb9193fba89ddf00c0cfcb2368ce9340c6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.notification.row;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.widget.FrameLayout;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.CancellationSignal;

import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;

/**
 * {@link NotifBindPipeline} is responsible for converting notifications from their data form to
 * their actual inflated views. It is essentially a control class that composes notification view
 * binding logic (i.e. {@link BindStage}) in response to explicit bind requests. At the end of the
 * pipeline, the notification's bound views are guaranteed to be correct and up-to-date, and any
 * registered callbacks will be called.
 *
 * The pipeline ensures that a notification's top-level view and its content views are bound.
 * Currently, a notification's top-level view, the {@link ExpandableNotificationRow} is essentially
 * just a {@link FrameLayout} for various different content views that are switched in and out as
 * appropriate. These include a contracted view, expanded view, heads up view, and sensitive view on
 * keyguard. See {@link InflationFlag}. These content views themselves can have child views added
 * on depending on different factors. For example, notification actions and smart replies are views
 * that are dynamically added to these content views after they're inflated. Finally, aside from
 * the app provided content views, System UI itself also provides some content views that are shown
 * occasionally (e.g. {@link NotificationGuts}). Many of these are business logic specific views
 * and the requirements surrounding them may change over time, so the pipeline must handle
 * composing the logic as necessary.
 *
 * Note that bind requests do not only occur from add/updates from updates from the app. For
 * example, the user may make changes to device settings (e.g. sensitive notifications on lock
 * screen) or we may want to make certain optimizations for the sake of memory or performance (e.g
 * freeing views when not visible). Oftentimes, we also need to wait for these changes to complete
 * before doing something else (e.g. moving a notification to the top of the screen to heads up).
 * The pipeline thus handles bind requests from across the system and provides a way for
 * requesters to know when the change is propagated to the view.
 *
 * Right now, we only support one attached {@link BindStage} which just does all the binding but we
 * should eventually support multiple stages once content inflation is made more modular.
 * In particular, row inflation/binding, which is handled by {@link NotificationRowBinder} should
 * probably be moved here in the future as a stage. Right now, the pipeline just manages content
 * views and assumes that a row is given to it when it's inflated.
 */
@MainThread
@SysUISingleton
public final class NotifBindPipeline {
    private final Map<NotificationEntry, BindEntry> mBindEntries = new ArrayMap<>();
    private final NotifBindPipelineLogger mLogger;
    private final List<BindCallback> mScratchCallbacksList = new ArrayList<>();
    private final Handler mMainHandler;
    private BindStage mStage;

    @Inject
    NotifBindPipeline(
            CommonNotifCollection collection,
            NotifBindPipelineLogger logger,
            @Main Looper mainLooper) {
        collection.addCollectionListener(mCollectionListener);
        mLogger = logger;
        mMainHandler = new NotifBindPipelineHandler(mainLooper);
    }

    /**
     * Set the bind stage for binding notification row content.
     */
    public void setStage(
            BindStage stage) {
        mLogger.logStageSet(stage.getClass().getName());

        mStage = stage;
        mStage.setBindRequestListener(this::onBindRequested);
    }

    /**
     * Start managing the row's content for a given notification.
     */
    public void manageRow(
            @NonNull NotificationEntry entry,
            @NonNull ExpandableNotificationRow row) {
        mLogger.logManagedRow(entry);
        mLogger.logManagedRow(entry);

        final BindEntry bindEntry = getBindEntry(entry);
        if (bindEntry == null) {
            return;
        }
        bindEntry.row = row;
        if (bindEntry.invalidated) {
            requestPipelineRun(entry);
        }
    }

    private void onBindRequested(
            @NonNull NotificationEntry entry,
            @NonNull CancellationSignal signal,
            @Nullable BindCallback callback) {
        final BindEntry bindEntry = getBindEntry(entry);
        if (bindEntry == null) {
            // Invalidating views for a notification that is not active.
            return;
        }

        bindEntry.invalidated = true;

        // Put in new callback.
        if (callback != null) {
            final Set<BindCallback> callbacks = bindEntry.callbacks;
            callbacks.add(callback);
            signal.setOnCancelListener(() -> callbacks.remove(callback));
        }

        requestPipelineRun(entry);
    }

    /**
     * Request pipeline to start.
     *
     * We avoid starting the pipeline immediately as multiple clients may request rebinds
     * back-to-back due to a single change (e.g. notification update), and it's better to start
     * the real work once rather than repeatedly start and cancel it.
     */
    private void requestPipelineRun(NotificationEntry entry) {
        mLogger.logRequestPipelineRun(entry);

        final BindEntry bindEntry = getBindEntry(entry);
        if (bindEntry.row == null) {
            // Row is not managed yet but may be soon. Stop for now.
            mLogger.logRequestPipelineRowNotSet(entry);
            return;
        }

        // Abort any existing pipeline run
        mStage.abortStage(entry, bindEntry.row);

        if (!mMainHandler.hasMessages(START_PIPELINE_MSG, entry)) {
            Message msg = Message.obtain(mMainHandler, START_PIPELINE_MSG, entry);
            mMainHandler.sendMessage(msg);
        }
    }

    /**
     * Run the pipeline for the notification, ensuring all views are bound when finished. Call all
     * callbacks when the run finishes. If a run is already in progress, it is restarted.
     */
    private void startPipeline(NotificationEntry entry) {
        mLogger.logStartPipeline(entry);

        if (mStage == null) {
            throw new IllegalStateException("No stage was ever set on the pipeline");
        }

        final BindEntry bindEntry = mBindEntries.get(entry);
        final ExpandableNotificationRow row = bindEntry.row;

        mStage.executeStage(entry, row, (en) -> onPipelineComplete(en));
    }

    private void onPipelineComplete(NotificationEntry entry) {
        final BindEntry bindEntry = getBindEntry(entry);
        final Set<BindCallback> callbacks = bindEntry.callbacks;

        mLogger.logFinishedPipeline(entry, callbacks.size());

        bindEntry.invalidated = false;
        // Move all callbacks to separate list as callbacks may themselves add/remove callbacks.
        // TODO: Throw an exception for this re-entrant behavior once we deprecate
        // NotificationGroupAlertTransferHelper
        mScratchCallbacksList.addAll(callbacks);
        callbacks.clear();
        for (int i = 0; i < mScratchCallbacksList.size(); i++) {
            mScratchCallbacksList.get(i).onBindFinished(entry);
        }
        mScratchCallbacksList.clear();
    }

    private final NotifCollectionListener mCollectionListener = new NotifCollectionListener() {
        @Override
        public void onEntryInit(NotificationEntry entry) {
            mBindEntries.put(entry, new BindEntry());
            mStage.createStageParams(entry);
        }

        @Override
        public void onEntryCleanUp(NotificationEntry entry) {
            BindEntry bindEntry = mBindEntries.remove(entry);
            ExpandableNotificationRow row = bindEntry.row;
            if (row != null) {
                mStage.abortStage(entry, row);
            }
            mStage.deleteStageParams(entry);
            mMainHandler.removeMessages(START_PIPELINE_MSG, entry);
        }
    };

    private @NonNull BindEntry getBindEntry(NotificationEntry entry) {
        final BindEntry bindEntry = mBindEntries.get(entry);
        return bindEntry;
    }

    /**
     * Interface for bind callback.
     */
    public interface BindCallback {
        /**
         * Called when all views are fully bound on the notification.
         */
        void onBindFinished(NotificationEntry entry);
    }

    private class BindEntry {
        public ExpandableNotificationRow row;
        public final Set<BindCallback> callbacks = new ArraySet<>();
        public boolean invalidated;
    }

    private static final int START_PIPELINE_MSG = 1;

    private class NotifBindPipelineHandler extends Handler {

        NotifBindPipelineHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case START_PIPELINE_MSG:
                    NotificationEntry entry = (NotificationEntry) msg.obj;
                    startPipeline(entry);
                    break;
                default:
                    throw new IllegalArgumentException("Unknown message type: " + msg.what);
            }
        }
    }
}