Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 131f30d1 authored by Kevin Han's avatar Kevin Han
Browse files

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
parent 3d358dba
Loading
Loading
Loading
Loading
+6 −1
Original line number Original line Diff line number Diff line
@@ -275,7 +275,12 @@ public final class NotificationEntry extends ListEntry {
        return mHasInflationError;
        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;
        mHasInflationError = hasError;
    }
    }


+76 −0
Original line number Original line Diff line number Diff line
/*
 * 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);

    }
}
+104 −0
Original line number Original line Diff line number Diff line
/*
 * 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);
    }
}
+207 −0
Original line number Original line Diff line number Diff line
/*
 * 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;
    }
}
+1 −1
Original line number Original line Diff line number Diff line
@@ -101,7 +101,7 @@ public interface NotificationRowContentBinder {
     */
     */
    int FLAG_CONTENT_VIEW_PUBLIC = 1 << 3;
    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
     * Parameters for content view binding
Loading