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 df65dacf12dd7..3f1eabb9df93c 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 0000000000000..1cf6b4f2321c2 --- /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 0000000000000..29447caa12407 --- /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 to do this stage + */ +@MainThread +public abstract class BindStage extends BindRequester { + + private Map 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 0000000000000..af2d0844412a0 --- /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 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 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 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 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 9b95bff4921c5..f90ec85b09d8c 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 0000000000000..bddbecc78bfbb --- /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 0000000000000..6b700cd50ca3a --- /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 { + 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 0000000000000..8f9f65d127624 --- /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 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 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 0000000000000..ce8f697556c60 --- /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 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 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 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 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()); + } +}