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