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