diff options
| author | 2020-01-06 14:11:28 -0800 | |
|---|---|---|
| committer | 2020-02-05 11:32:48 -0800 | |
| commit | 131f30d168a0bea83d4cda1f36539a17a74c317b (patch) | |
| tree | aa8183769ad020cda362f8822cc44f652b554cf1 | |
| parent | 3d358dba3d5ae8675b6093e5f92d08b33aad7185 (diff) | |
Introduce NotifBindPipeline (1/2)
Introduce NotifBindPipeline and friends which perform the following
roles.
NotifBindPipeline
* Knows when all view inflation + bind logic is done and calls callbacks
* Composes BindStage(s) to bind notif views when something changes
RowContentBindParams
* New place for inflation-related params in ExpandableNotificationRow to
live (inflation flags, increasedContentHeight, lowPriority)
RowContentBindStage
* Abstracted stage of work for pipeline to use that wraps around
NotificationRowContentBinderImpl in order to bind content.
* Provides params objects that clients can modify
* Provides invalidate call as part of BindStage that starts pipeline
This CL simply introduces the classes but does not use them. The latter
CL will focus on swapping out the existing NotificationRowContentBinder
API usage in ExpandableNotificationRow and attaching these classes.
See design at go/notification-bind-pipeline.
Bug: 145749521
Test: builds on wembley, crosshatch
Test: atest SystemUITests
Change-Id: I8366010afad1c8bd2d76cbe71d21b55bbbb11bbe
9 files changed, 1036 insertions, 2 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index df65dacf12dd..3f1eabb9df93 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -275,7 +275,12 @@ public final class NotificationEntry extends ListEntry { return mHasInflationError; } - void setHasInflationError(boolean hasError) { + /** + * Set whether the notification has an error while inflating. + * + * TODO: Move this into an inflation error manager class. + */ + public void setHasInflationError(boolean hasError) { mHasInflationError = hasError; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindRequester.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindRequester.java new file mode 100644 index 000000000000..1cf6b4f2321c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindRequester.java @@ -0,0 +1,76 @@ +/* + * 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.os.CancellationSignal; + +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback; + +/** + * A {@link BindRequester} is a general superclass for something that notifies + * {@link NotifBindPipeline} when it needs it to kick off a bind run. + */ +public abstract class BindRequester { + private @Nullable BindRequestListener mBindRequestListener; + + /** + * Notifies the listener that some parameters/state has changed for some notification and that + * content needs to be bound again. + * + * The caller can also specify a callback for when the entire bind pipeline completes, i.e. + * when the change is fully propagated to the final view. The caller can cancel this + * callback with the returned cancellation signal. + * + * @param callback callback after bind completely finishes + * @return cancellation signal to cancel callback + */ + public final CancellationSignal requestRebind( + @NonNull NotificationEntry entry, + @Nullable BindCallback callback) { + CancellationSignal signal = new CancellationSignal(); + if (mBindRequestListener != null) { + mBindRequestListener.onBindRequest(entry, signal, callback); + } + return signal; + } + + final void setBindRequestListener(BindRequestListener listener) { + mBindRequestListener = listener; + } + + /** + * Listener interface for when content needs to be bound again. + */ + public interface BindRequestListener { + + /** + * Called when {@link #requestRebind} is called. + * + * @param entry notification that has outdated content + * @param signal cancellation signal to cancel callback + * @param callback callback after content is fully updated + */ + void onBindRequest( + @NonNull NotificationEntry entry, + @NonNull CancellationSignal signal, + @Nullable BindCallback callback); + + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java new file mode 100644 index 000000000000..29447caa1240 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java @@ -0,0 +1,104 @@ +/* + * 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.annotation.MainThread; +import android.util.ArrayMap; + +import androidx.annotation.NonNull; + +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +import java.util.Map; + +/** + * A {@link BindStage} is an abstraction for a unit of work in inflating/binding/unbinding + * views to a notification. Used by {@link NotifBindPipeline}. + * + * Clients may also use {@link #getStageParams} to provide parameters for this stage for a given + * notification and request a rebind. + * + * @param <Params> params to do this stage + */ +@MainThread +public abstract class BindStage<Params> extends BindRequester { + + private Map<NotificationEntry, Params> mContentParams = new ArrayMap<>(); + + /** + * Execute the stage asynchronously. + * + * @param row notification top-level view to bind views to + * @param callback callback after stage finishes + */ + protected abstract void executeStage( + @NonNull NotificationEntry entry, + @NonNull ExpandableNotificationRow row, + @NonNull StageCallback callback); + + /** + * Abort the stage if in progress. + * + * @param row notification top-level view to bind views to + */ + protected abstract void abortStage( + @NonNull NotificationEntry entry, + @NonNull ExpandableNotificationRow row); + + /** + * Get the stage parameters for the entry. Clients should use this to modify how the stage + * handles the notification content. + */ + public final Params getStageParams(@NonNull NotificationEntry entry) { + Params params = mContentParams.get(entry); + if (params == null) { + throw new IllegalStateException( + String.format("Entry does not have any stage parameters. key: %s", + entry.getKey())); + } + return params; + } + + /** + * Create a params entry for the notification for this stage. + */ + final void createStageParams(@NonNull NotificationEntry entry) { + mContentParams.put(entry, newStageParams()); + } + + /** + * Delete params entry for notification. + */ + final void deleteStageParams(@NonNull NotificationEntry entry) { + mContentParams.remove(entry); + } + + /** + * Create a new, empty stage params object. + */ + protected abstract Params newStageParams(); + + /** + * Interface for callback. + */ + interface StageCallback { + /** + * Callback for when the stage is complete. + */ + void onStageFinished(NotificationEntry entry); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java new file mode 100644 index 000000000000..af2d0844412a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java @@ -0,0 +1,207 @@ +/* + * 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.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.internal.statusbar.NotificationVisibility; +import com.android.systemui.statusbar.notification.NotificationEntryListener; +import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder; +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; + +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * {@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 +@Singleton +public final class NotifBindPipeline { + private final Map<NotificationEntry, BindEntry> mBindEntries = new ArrayMap<>(); + private BindStage mStage; + + @Inject + NotifBindPipeline(NotificationEntryManager entryManager) { + entryManager.addNotificationEntryListener(mEntryListener); + } + + /** + * Set the bind stage for binding notification row content. + */ + public void setStage( + BindStage stage) { + 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) { + final BindEntry bindEntry = getBindEntry(entry); + bindEntry.row = row; + if (bindEntry.invalidated) { + startPipeline(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)); + } + + startPipeline(entry); + } + + /** + * 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) { + 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; + if (row == null) { + // Row is not managed yet but may be soon. Stop for now. + return; + } + + mStage.abortStage(entry, row); + mStage.executeStage(entry, row, (en) -> onPipelineComplete(en)); + } + + private void onPipelineComplete(NotificationEntry entry) { + final BindEntry bindEntry = getBindEntry(entry); + + bindEntry.invalidated = false; + + final Set<BindCallback> callbacks = bindEntry.callbacks; + for (BindCallback cb : callbacks) { + cb.onBindFinished(entry); + } + callbacks.clear(); + } + + //TODO: Move this to onManageEntry hook when we split that from add/remove + private final NotificationEntryListener mEntryListener = new NotificationEntryListener() { + @Override + public void onPendingEntryAdded(NotificationEntry entry) { + mBindEntries.put(entry, new BindEntry()); + mStage.createStageParams(entry); + } + + @Override + public void onEntryRemoved(NotificationEntry entry, + @Nullable NotificationVisibility visibility, + boolean removedByUser) { + BindEntry bindEntry = mBindEntries.remove(entry); + ExpandableNotificationRow row = bindEntry.row; + if (row != null) { + mStage.abortStage(entry, row); + } + mStage.deleteStageParams(entry); + } + }; + + private @NonNull BindEntry getBindEntry(NotificationEntry entry) { + final BindEntry bindEntry = mBindEntries.get(entry); + if (bindEntry == null) { + throw new IllegalStateException( + String.format("Attempting bind on an inactive notification. key: %s", + entry.getKey())); + } + 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; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java index 9b95bff4921c..f90ec85b09d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java @@ -101,7 +101,7 @@ public interface NotificationRowContentBinder { */ int FLAG_CONTENT_VIEW_PUBLIC = 1 << 3; - int FLAG_CONTENT_VIEW_ALL = ~0; + int FLAG_CONTENT_VIEW_ALL = (1 << 4) - 1; /** * Parameters for content view binding diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java new file mode 100644 index 000000000000..bddbecc78bfb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java @@ -0,0 +1,147 @@ +/* + * 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 static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; + +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; + +/** + * Parameters for {@link RowContentBindStage}. + */ +final class RowContentBindParams { + + private boolean mUseLowPriority; + private boolean mUseChildInGroup; + private boolean mUseIncreasedHeight; + private boolean mUseIncreasedHeadsUpHeight; + private @InflationFlag int mContentViews; + private boolean mViewsNeedReinflation; + + /** + * Content views that are out of date and need to be rebound. + * + * TODO: This should go away once {@link NotificationContentInflater} is broken down into + * smaller stages as then the stage itself would be invalidated. + */ + private @InflationFlag int mDirtyContentViews; + + /** + * Set whether content should use a low priority version of its content views. + */ + public void setUseLowPriority(boolean useLowPriority) { + if (mUseLowPriority != useLowPriority) { + mDirtyContentViews |= (FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED); + } + mUseLowPriority = useLowPriority; + } + + public boolean useLowPriority() { + return mUseLowPriority; + } + + /** + * Set whether content should use group child version of its content views. + */ + public void setUseChildInGroup(boolean useChildInGroup) { + if (mUseChildInGroup != useChildInGroup) { + mDirtyContentViews |= (FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED); + } + mUseChildInGroup = useChildInGroup; + } + + public boolean useChildInGroup() { + return mUseChildInGroup; + } + + /** + * Set whether content should use an increased height version of its contracted view. + */ + public void setUseIncreasedHeight(boolean useIncreasedHeight) { + if (mUseIncreasedHeight != useIncreasedHeight) { + mDirtyContentViews |= FLAG_CONTENT_VIEW_CONTRACTED; + } + mUseIncreasedHeight = useIncreasedHeight; + } + + public boolean useIncreasedHeight() { + return mUseIncreasedHeight; + } + + /** + * Set whether content should use an increased height version of its heads up view. + */ + public void setUseIncreasedHeadsUpHeight(boolean useIncreasedHeadsUpHeight) { + if (mUseIncreasedHeadsUpHeight != useIncreasedHeadsUpHeight) { + mDirtyContentViews |= FLAG_CONTENT_VIEW_HEADS_UP; + } + mUseIncreasedHeadsUpHeight = useIncreasedHeadsUpHeight; + } + + public boolean useIncreasedHeadsUpHeight() { + return mUseIncreasedHeadsUpHeight; + } + + /** + * Set whether the specified content views should be bound. See {@link InflationFlag}. + */ + public void setShouldContentViewsBeBound( + @InflationFlag int contentViews, + boolean shouldBeBound) { + if (shouldBeBound) { + @InflationFlag int newContentViews = contentViews &= ~mContentViews; + mContentViews |= contentViews; + mDirtyContentViews |= newContentViews; + } else { + mContentViews &= ~contentViews; + mDirtyContentViews &= ~contentViews; + } + } + + public @InflationFlag int getContentViews() { + return mContentViews; + } + + /** + * Clears all dirty content views so that they no longer need to be rebound. + */ + void clearDirtyContentViews() { + mDirtyContentViews = 0; + } + + public @InflationFlag int getDirtyContentViews() { + return mDirtyContentViews; + } + + /** + * Set whether all content views need to be reinflated even if cached. + * + * TODO: This should probably be a more global config on {@link NotifBindPipeline} since this + * generally corresponds to a Context/Configuration change that all stages should know about. + */ + public void setNeedsReinflation(boolean needsReinflation) { + mViewsNeedReinflation = needsReinflation; + @InflationFlag int currentContentViews = mContentViews; + mDirtyContentViews |= currentContentViews; + } + + public boolean needsReinflation() { + return mViewsNeedReinflation; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java new file mode 100644 index 000000000000..6b700cd50ca3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -0,0 +1,119 @@ +/* + * 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 static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; + +import android.os.RemoteException; +import android.service.notification.StatusBarNotification; + +import androidx.annotation.NonNull; + +import com.android.internal.statusbar.IStatusBarService; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams; +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback; +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * A stage that binds all content views for an already inflated {@link ExpandableNotificationRow}. + * + * In the farther future, the binder logic and consequently this stage should be broken into + * smaller stages. + */ +@Singleton +public class RowContentBindStage extends BindStage<RowContentBindParams> { + private final NotificationRowContentBinder mBinder; + private final IStatusBarService mStatusBarService; + + @Inject + RowContentBindStage( + NotificationRowContentBinder binder, + IStatusBarService statusBarService) { + mBinder = binder; + mStatusBarService = statusBarService; + } + + @Override + protected void executeStage( + @NonNull NotificationEntry entry, + @NonNull ExpandableNotificationRow row, + @NonNull StageCallback callback) { + RowContentBindParams params = getStageParams(entry); + + // Resolve content to bind/unbind. + @InflationFlag int inflationFlags = params.getContentViews(); + @InflationFlag int invalidatedFlags = params.getDirtyContentViews(); + + @InflationFlag int contentToBind = invalidatedFlags & inflationFlags; + @InflationFlag int contentToUnbind = inflationFlags ^ FLAG_CONTENT_VIEW_ALL; + + // Bind/unbind with parameters + mBinder.unbindContent(entry, row, contentToUnbind); + + BindParams bindParams = new BindParams(); + bindParams.isLowPriority = params.useLowPriority(); + bindParams.isChildInGroup = params.useChildInGroup(); + bindParams.usesIncreasedHeight = params.useIncreasedHeight(); + bindParams.usesIncreasedHeadsUpHeight = params.useIncreasedHeadsUpHeight(); + boolean forceInflate = params.needsReinflation(); + + InflationCallback inflationCallback = new InflationCallback() { + @Override + public void handleInflationException(NotificationEntry entry, Exception e) { + entry.setHasInflationError(true); + try { + final StatusBarNotification sbn = entry.getSbn(); + mStatusBarService.onNotificationError( + sbn.getPackageName(), + sbn.getTag(), + sbn.getId(), + sbn.getUid(), + sbn.getInitialPid(), + e.getMessage(), + sbn.getUserId()); + } catch (RemoteException ex) { + } + } + + @Override + public void onAsyncInflationFinished(NotificationEntry entry, + @InflationFlag int inflatedFlags) { + entry.setHasInflationError(false); + getStageParams(entry).clearDirtyContentViews(); + callback.onStageFinished(entry); + } + }; + mBinder.cancelBind(entry, row); + mBinder.bindContent(entry, row, contentToBind, bindParams, forceInflate, inflationCallback); + } + + @Override + protected void abortStage( + @NonNull NotificationEntry entry, + @NonNull ExpandableNotificationRow row) { + mBinder.cancelBind(entry, row); + } + + @Override + protected RowContentBindParams newStageParams() { + return new RowContentBindParams(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineTest.java new file mode 100644 index 000000000000..8f9f65d12762 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2019 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 static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.annotation.NonNull; +import androidx.core.os.CancellationSignal; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.NotificationEntryListener; +import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class NotifBindPipelineTest extends SysuiTestCase { + + private NotifBindPipeline mBindPipeline; + private TestBindStage mStage = new TestBindStage(); + + @Mock private NotificationEntry mEntry; + @Mock private ExpandableNotificationRow mRow; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + NotificationEntryManager entryManager = mock(NotificationEntryManager.class); + + mBindPipeline = new NotifBindPipeline(entryManager); + mBindPipeline.setStage(mStage); + + ArgumentCaptor<NotificationEntryListener> entryListenerCaptor = + ArgumentCaptor.forClass(NotificationEntryListener.class); + verify(entryManager).addNotificationEntryListener(entryListenerCaptor.capture()); + NotificationEntryListener entryListener = entryListenerCaptor.getValue(); + + entryListener.onPendingEntryAdded(mEntry); + } + + @Test + public void testCallbackCalled() { + // GIVEN a bound row + mBindPipeline.manageRow(mEntry, mRow); + + // WHEN content is invalidated + BindCallback callback = mock(BindCallback.class); + mStage.requestRebind(mEntry, callback); + + // WHEN stage finishes its work + mStage.doWorkSynchronously(); + + // THEN the callback is called when bind finishes + verify(callback).onBindFinished(mEntry); + } + + @Test + public void testCallbackCancelled() { + // GIVEN a bound row + mBindPipeline.manageRow(mEntry, mRow); + + // GIVEN an in-progress pipeline run + BindCallback callback = mock(BindCallback.class); + CancellationSignal signal = mStage.requestRebind(mEntry, callback); + + // WHEN the callback is cancelled. + signal.cancel(); + + // WHEN the stage finishes all its work + mStage.doWorkSynchronously(); + + // THEN the callback is not called when bind finishes + verify(callback, never()).onBindFinished(mEntry); + } + + @Test + public void testMultipleCallbacks() { + // GIVEN a bound row + mBindPipeline.manageRow(mEntry, mRow); + + // WHEN the pipeline is invalidated. + BindCallback callback = mock(BindCallback.class); + mStage.requestRebind(mEntry, callback); + + // WHEN the pipeline is invalidated again before the work completes. + BindCallback callback2 = mock(BindCallback.class); + mStage.requestRebind(mEntry, callback2); + + // WHEN the stage finishes all work. + mStage.doWorkSynchronously(); + + // THEN both callbacks are called when the bind finishes + verify(callback).onBindFinished(mEntry); + verify(callback2).onBindFinished(mEntry); + } + + /** + * Bind stage for testing where asynchronous work can be synchronously controlled. + */ + private static class TestBindStage extends BindStage { + private List<Runnable> mExecutionRequests = new ArrayList<>(); + + @Override + protected void executeStage(@NonNull NotificationEntry entry, + @NonNull ExpandableNotificationRow row, @NonNull StageCallback callback) { + mExecutionRequests.add(() -> callback.onStageFinished(entry)); + } + + @Override + protected void abortStage(@NonNull NotificationEntry entry, + @NonNull ExpandableNotificationRow row) { + + } + + @Override + protected Object newStageParams() { + return null; + } + + public void doWorkSynchronously() { + for (Runnable work: mExecutionRequests) { + work.run(); + } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java new file mode 100644 index 000000000000..ce8f697556c6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java @@ -0,0 +1,218 @@ +/* + * 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 static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; + +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.internal.statusbar.IStatusBarService; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class RowContentBindStageTest extends SysuiTestCase { + + private RowContentBindStage mRowContentBindStage; + + @Mock private NotificationRowContentBinder mBinder; + @Mock private NotificationEntry mEntry; + @Mock private ExpandableNotificationRow mRow; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mRowContentBindStage = new RowContentBindStage(mBinder, + mock(IStatusBarService.class)); + mRowContentBindStage.createStageParams(mEntry); + } + + @Test + public void testSetShouldContentViewsBeBound_bindsContent() { + // WHEN inflation flags are set and pipeline is invalidated. + final int flags = FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED; + RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); + params.setShouldContentViewsBeBound(flags, true /* shouldBeBound */); + mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { }); + + // THEN binder binds inflation flags. + verify(mBinder).bindContent( + eq(mEntry), + any(), + eq(flags), + any(), + anyBoolean(), + any()); + } + + @Test + public void testSetShouldContentViewsBeBound_unbindsContent() { + // GIVEN a view with all content bound. + RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); + params.setShouldContentViewsBeBound(FLAG_CONTENT_VIEW_ALL, true /* shouldBeBound */); + + // WHEN inflation flags are cleared and stage executed. + final int flags = FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED; + params.setShouldContentViewsBeBound(flags, false /* shouldBeBound */); + mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { }); + + // THEN binder unbinds flags. + verify(mBinder).unbindContent(eq(mEntry), any(), eq(flags)); + } + + @Test + public void testSetUseLowPriority() { + // GIVEN a view with all content bound. + RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); + params.setShouldContentViewsBeBound(FLAG_CONTENT_VIEW_ALL, true /* shouldBeBound */); + params.clearDirtyContentViews(); + + // WHEN low priority is set and stage executed. + params.setUseLowPriority(true); + mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { }); + + // THEN binder is called with use low priority and contracted/expanded are called to bind. + ArgumentCaptor<BindParams> bindParamsCaptor = ArgumentCaptor.forClass(BindParams.class); + verify(mBinder).bindContent( + eq(mEntry), + any(), + eq(FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED), + bindParamsCaptor.capture(), + anyBoolean(), + any()); + BindParams usedParams = bindParamsCaptor.getValue(); + assertTrue(usedParams.isLowPriority); + } + + @Test + public void testSetUseGroupInChild() { + // GIVEN a view with all content bound. + RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); + params.setShouldContentViewsBeBound(FLAG_CONTENT_VIEW_ALL, true /* shouldBeBound */); + params.clearDirtyContentViews(); + + // WHEN use group is set and stage executed. + params.setUseChildInGroup(true); + mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { }); + + // THEN binder is called with use group view and contracted/expanded are called to bind. + ArgumentCaptor<BindParams> bindParamsCaptor = ArgumentCaptor.forClass(BindParams.class); + verify(mBinder).bindContent( + eq(mEntry), + any(), + eq(FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED), + bindParamsCaptor.capture(), + anyBoolean(), + any()); + BindParams usedParams = bindParamsCaptor.getValue(); + assertTrue(usedParams.isChildInGroup); + } + + @Test + public void testSetUseIncreasedHeight() { + // GIVEN a view with all content bound. + RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); + params.setShouldContentViewsBeBound(FLAG_CONTENT_VIEW_ALL, true /* shouldBeBound */); + params.clearDirtyContentViews(); + + // WHEN use increased height is set and stage executed. + params.setUseIncreasedHeight(true); + mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { }); + + // THEN binder is called with group view and contracted is bound. + ArgumentCaptor<BindParams> bindParamsCaptor = ArgumentCaptor.forClass(BindParams.class); + verify(mBinder).bindContent( + eq(mEntry), + any(), + eq(FLAG_CONTENT_VIEW_CONTRACTED), + bindParamsCaptor.capture(), + anyBoolean(), + any()); + BindParams usedParams = bindParamsCaptor.getValue(); + assertTrue(usedParams.usesIncreasedHeight); + } + + @Test + public void testSetUseIncreasedHeadsUpHeight() { + // GIVEN a view with all content bound. + RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); + params.setShouldContentViewsBeBound(FLAG_CONTENT_VIEW_ALL, true /* shouldBeBound */); + params.clearDirtyContentViews(); + + // WHEN use increased heads up height is set and stage executed. + params.setUseIncreasedHeadsUpHeight(true); + mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { }); + + // THEN binder is called with use group view and heads up is bound. + ArgumentCaptor<BindParams> bindParamsCaptor = ArgumentCaptor.forClass(BindParams.class); + verify(mBinder).bindContent( + eq(mEntry), + any(), + eq(FLAG_CONTENT_VIEW_HEADS_UP), + bindParamsCaptor.capture(), + anyBoolean(), + any()); + BindParams usedParams = bindParamsCaptor.getValue(); + assertTrue(usedParams.usesIncreasedHeadsUpHeight); + } + + @Test + public void testSetNeedsReinflation() { + // GIVEN a view with all content bound. + RowContentBindParams params = mRowContentBindStage.getStageParams(mEntry); + params.setShouldContentViewsBeBound(FLAG_CONTENT_VIEW_ALL, true /* shouldBeBound */); + params.clearDirtyContentViews(); + + // WHEN needs reinflation is set. + params.setNeedsReinflation(true); + mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { }); + + // THEN binder is called with forceInflate and all views are requested to bind. + verify(mBinder).bindContent( + eq(mEntry), + any(), + eq(FLAG_CONTENT_VIEW_ALL), + any(), + eq(true), + any()); + } +} |