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