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

Commit f26444d7 authored by Sam Solomon's avatar Sam Solomon Committed by Automerger Merge Worker
Browse files

Merge "Wait for Folding Feature State" into tm-qpr-dev am: 1189c482

parents 117ef108 1189c482
Loading
Loading
Loading
Loading
+88 −28
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
import static androidx.window.common.CommonFoldingFeature.parseListFromString;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
@@ -30,22 +29,25 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;

import androidx.window.util.AcceptOnceConsumer;
import androidx.window.util.BaseDataProducer;
import androidx.window.util.DataProducer;

import com.android.internal.R;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

/**
 * An implementation of {@link androidx.window.util.DataProducer} that returns the device's posture
 * by mapping the state returned from {@link DeviceStateManager} to values provided in the resources
 * config at {@link R.array#config_device_state_postures}.
 * An implementation of {@link androidx.window.util.BaseDataProducer} that returns
 * the device's posture by mapping the state returned from {@link DeviceStateManager} to
 * values provided in the resources' config at {@link R.array#config_device_state_postures}.
 */
public final class DeviceStateManagerFoldingFeatureProducer extends
        BaseDataProducer<List<CommonFoldingFeature>> {
public final class DeviceStateManagerFoldingFeatureProducer
        extends BaseDataProducer<List<CommonFoldingFeature>> {
    private static final String TAG =
            DeviceStateManagerFoldingFeatureProducer.class.getSimpleName();
    private static final boolean DEBUG = false;
@@ -54,15 +56,11 @@ public final class DeviceStateManagerFoldingFeatureProducer extends

    private int mCurrentDeviceState = INVALID_DEVICE_STATE;

    private final DeviceStateCallback mDeviceStateCallback = (state) -> {
        mCurrentDeviceState = state;
        notifyDataChanged();
    };
    @NonNull
    private final DataProducer<String> mRawFoldSupplier;
    private final BaseDataProducer<String> mRawFoldSupplier;

    public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context,
            @NonNull DataProducer<String> rawFoldSupplier) {
            @NonNull BaseDataProducer<String> rawFoldSupplier) {
        mRawFoldSupplier = rawFoldSupplier;
        String[] deviceStatePosturePairs = context.getResources()
                .getStringArray(R.array.config_device_state_postures);
@@ -70,7 +68,8 @@ public final class DeviceStateManagerFoldingFeatureProducer extends
            String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
            if (deviceStatePostureMapping.length != 2) {
                if (DEBUG) {
                    Log.e(TAG, "Malformed device state posture pair: " + deviceStatePosturePair);
                    Log.e(TAG, "Malformed device state posture pair: "
                            + deviceStatePosturePair);
                }
                continue;
            }
@@ -82,7 +81,8 @@ public final class DeviceStateManagerFoldingFeatureProducer extends
                posture = Integer.parseInt(deviceStatePostureMapping[1]);
            } catch (NumberFormatException e) {
                if (DEBUG) {
                    Log.e(TAG, "Failed to parse device state or posture: " + deviceStatePosturePair,
                    Log.e(TAG, "Failed to parse device state or posture: "
                                    + deviceStatePosturePair,
                            e);
                }
                continue;
@@ -92,30 +92,90 @@ public final class DeviceStateManagerFoldingFeatureProducer extends
        }

        if (mDeviceStateToPostureMap.size() > 0) {
            context.getSystemService(DeviceStateManager.class)
                    .registerCallback(context.getMainExecutor(), mDeviceStateCallback);
            DeviceStateCallback deviceStateCallback = (state) -> {
                mCurrentDeviceState = state;
                mRawFoldSupplier.getData(this::notifyFoldingFeatureChange);
            };
            Objects.requireNonNull(context.getSystemService(DeviceStateManager.class))
                    .registerCallback(context.getMainExecutor(), deviceStateCallback);
        }
    }

    @Override
    @Nullable
    public Optional<List<CommonFoldingFeature>> getData() {
        final int globalHingeState = globalHingeState();
        Optional<String> displayFeaturesString = mRawFoldSupplier.getData();
        if (displayFeaturesString.isEmpty() || TextUtils.isEmpty(displayFeaturesString.get())) {
            return Optional.empty();
    /**
     * Add a callback to mCallbacks if there is no device state. This callback will be run
     * once a device state is set. Otherwise,run the callback immediately.
     */
    private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback,
            String displayFeaturesString) {
        if (isCurrentStateValid()) {
            callback.accept(calculateFoldingFeature(displayFeaturesString));
        } else {
            // This callback will be added to mCallbacks and removed once it runs once.
            AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
                    new AcceptOnceConsumer<>(this, callback);
            addDataChangedCallback(singleRunCallback);
        }
    }
        return Optional.of(parseListFromString(displayFeaturesString.get(), globalHingeState));

    /**
     * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the
     * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was
     * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}.
     * Returns a boolean value of whether the device state is valid.
     */
    private boolean isCurrentStateValid() {
        // If the device state is not found in the map, indexOfKey returns a negative number.
        return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState) >= 0;
    }

    @Override
    protected void onListenersChanged(Set<Runnable> callbacks) {
    protected void onListenersChanged(
            @NonNull Set<Consumer<List<CommonFoldingFeature>>> callbacks) {
        super.onListenersChanged(callbacks);
        if (callbacks.isEmpty()) {
            mRawFoldSupplier.removeDataChangedCallback(this::notifyDataChanged);
            mCurrentDeviceState = INVALID_DEVICE_STATE;
            mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
        } else {
            mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
        }
    }

    @NonNull
    @Override
    public Optional<List<CommonFoldingFeature>> getCurrentData() {
        Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
        if (!isCurrentStateValid()) {
            return Optional.empty();
        } else {
            mRawFoldSupplier.addDataChangedCallback(this::notifyDataChanged);
            return displayFeaturesString.map(this::calculateFoldingFeature);
        }
    }

    /**
     * Adds the data to the storeFeaturesConsumer when the data is ready.
     * @param storeFeaturesConsumer a consumer to collect the data when it is first available.
     */
    public void getData(Consumer<List<CommonFoldingFeature>> storeFeaturesConsumer) {
        mRawFoldSupplier.getData((String displayFeaturesString) -> {
            if (TextUtils.isEmpty(displayFeaturesString)) {
                storeFeaturesConsumer.accept(new ArrayList<>());
            } else {
                runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString);
            }
        });
    }

    private void notifyFoldingFeatureChange(String displayFeaturesString) {
        if (TextUtils.isEmpty(displayFeaturesString)) {
            notifyDataChanged(new ArrayList<>());
        } else {
            notifyDataChanged(calculateFoldingFeature(displayFeaturesString));
        }
    }

    private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) {
        final int globalHingeState = globalHingeState();
        return parseListFromString(displayFeaturesString, globalHingeState);
    }

    private int globalHingeState() {
+15 −7
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import com.android.internal.R;

import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Implementation of {@link androidx.window.util.DataProducer} that produces a
@@ -40,7 +41,7 @@ import java.util.Set;
 * settings where the {@link String} property is saved with the key
 * {@link RawFoldingFeatureProducer#DISPLAY_FEATURES}. If this value is null or empty then the
 * value in {@link android.content.res.Resources} is used. If both are empty then
 * {@link RawFoldingFeatureProducer#getData()} returns an empty object.
 * {@link RawFoldingFeatureProducer#getData} returns an empty object.
 * {@link RawFoldingFeatureProducer} listens to changes in the setting so that it can override
 * the system {@link CommonFoldingFeature} data.
 */
@@ -63,12 +64,13 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> {

    @Override
    @NonNull
    public Optional<String> getData() {
    public void getData(Consumer<String> dataConsumer) {
        String displayFeaturesString = getFeatureString();
        if (displayFeaturesString == null) {
            return Optional.empty();
            dataConsumer.accept("");
        } else {
            dataConsumer.accept(displayFeaturesString);
        }
        return Optional.of(displayFeaturesString);
    }

    /**
@@ -84,7 +86,7 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> {
    }

    @Override
    protected void onListenersChanged(Set<Runnable> callbacks) {
    protected void onListenersChanged(Set<Consumer<String>> callbacks) {
        if (callbacks.isEmpty()) {
            unregisterObserversIfNeeded();
        } else {
@@ -92,6 +94,12 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> {
        }
    }

    @NonNull
    @Override
    public Optional<String> getCurrentData() {
        return Optional.of(getFeatureString());
    }

    /**
     * Registers settings observers, if needed. When settings observers are registered for this
     * producer callbacks for changes in data will be triggered.
@@ -125,7 +133,7 @@ public final class RawFoldingFeatureProducer extends BaseDataProducer<String> {
        @Override
        public void onChange(boolean selfChange, Uri uri) {
            if (mDisplayFeaturesUri.equals(uri)) {
                notifyDataChanged();
                notifyDataChanged(getFeatureString());
            }
        }
    }
+27 −37
Original line number Diff line number Diff line
@@ -43,7 +43,6 @@ import androidx.window.util.DataProducer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

@@ -63,7 +62,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {

    private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer;

    public WindowLayoutComponentImpl(Context context) {
    public WindowLayoutComponentImpl(@NonNull Context context) {
        ((Application) context.getApplicationContext())
                .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged());
        RawFoldingFeatureProducer foldingFeatureProducer = new RawFoldingFeatureProducer(context);
@@ -81,7 +80,6 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
    public void addWindowLayoutInfoListener(@NonNull Activity activity,
            @NonNull Consumer<WindowLayoutInfo> consumer) {
        mWindowLayoutChangeListeners.put(activity, consumer);
        onDisplayFeaturesChanged();
    }

    /**
@@ -89,18 +87,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
     *
     * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo}
     */
    public void removeWindowLayoutInfoListener(
            @NonNull Consumer<WindowLayoutInfo> consumer) {
    public void removeWindowLayoutInfoListener(@NonNull Consumer<WindowLayoutInfo> consumer) {
        mWindowLayoutChangeListeners.values().remove(consumer);
        onDisplayFeaturesChanged();
    }

    void updateWindowLayout(@NonNull Activity activity,
            @NonNull WindowLayoutInfo newLayout) {
        Consumer<WindowLayoutInfo> consumer = mWindowLayoutChangeListeners.get(activity);
        if (consumer != null) {
            consumer.accept(newLayout);
        }
    }

    @NonNull
@@ -108,7 +96,6 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
        return mWindowLayoutChangeListeners.keySet();
    }

    @NonNull
    private boolean isListeningForLayoutChanges(IBinder token) {
        for (Activity activity: getActivitiesListeningForLayoutChanges()) {
            if (token.equals(activity.getWindow().getAttributes().token)) {
@@ -125,12 +112,12 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
    /**
     * A convenience method to translate from the common feature state to the extensions feature
     * state.  More specifically, translates from {@link CommonFoldingFeature.State} to
     * {@link FoldingFeature.STATE_FLAT} or {@link FoldingFeature.STATE_HALF_OPENED}. If it is not
     * {@link FoldingFeature#STATE_FLAT} or {@link FoldingFeature#STATE_HALF_OPENED}. If it is not
     * possible to translate, then we will return a {@code null} value.
     *
     * @param state if it matches a value in {@link CommonFoldingFeature.State}, {@code null}
     *              otherwise. @return a {@link FoldingFeature.STATE_FLAT} or
     *              {@link FoldingFeature.STATE_HALF_OPENED} if the given state matches a value in
     *              otherwise. @return a {@link FoldingFeature#STATE_FLAT} or
     *              {@link FoldingFeature#STATE_HALF_OPENED} if the given state matches a value in
     *              {@link CommonFoldingFeature.State} and {@code null} otherwise.
     */
    @Nullable
@@ -144,17 +131,24 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
        }
    }

    private void onDisplayFeaturesChanged() {
    private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) {
        for (Activity activity : getActivitiesListeningForLayoutChanges()) {
            WindowLayoutInfo newLayout = getWindowLayoutInfo(activity);
            updateWindowLayout(activity, newLayout);
            // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer.
            Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(activity);
            WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(activity, storedFeatures);
            layoutConsumer.accept(newWindowLayout);
        }
    }

    @NonNull
    private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
        List<DisplayFeature> displayFeatures = getDisplayFeatures(activity);
        return new WindowLayoutInfo(displayFeatures);
    /**
     * Translates the {@link DisplayFeature} into a {@link WindowLayoutInfo} when a
     * valid state is found.
     * @param activity a proxy for the {@link android.view.Window} that contains the
     */
    private WindowLayoutInfo getWindowLayoutInfo(
            @NonNull Activity activity, List<CommonFoldingFeature> storedFeatures) {
        List<DisplayFeature> displayFeatureList = getDisplayFeatures(activity, storedFeatures);
        return new WindowLayoutInfo(displayFeatureList);
    }

    /**
@@ -172,26 +166,21 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
     *
     * @param activity a proxy for the {@link android.view.Window} that contains the
     * {@link DisplayFeature}.
     * @return a {@link List} of valid {@link DisplayFeature} that
     * are within the {@link android.view.Window} of the {@link Activity}
     */
    private List<DisplayFeature> getDisplayFeatures(@NonNull Activity activity) {
    private List<DisplayFeature> getDisplayFeatures(
            @NonNull Activity activity, List<CommonFoldingFeature> storedFeatures) {
        List<DisplayFeature> features = new ArrayList<>();
        int displayId = activity.getDisplay().getDisplayId();
        if (displayId != DEFAULT_DISPLAY) {
            Log.w(TAG, "This sample doesn't support display features on secondary displays");
            return features;
        }

        if (activity.isInMultiWindowMode()) {
        } else if (activity.isInMultiWindowMode()) {
            // It is recommended not to report any display features in multi-window mode, since it
            // won't be possible to synchronize the display feature positions with window movement.
            return features;
        }

        Optional<List<CommonFoldingFeature>> storedFeatures = mFoldingFeatureProducer.getData();
        if (storedFeatures.isPresent()) {
            for (CommonFoldingFeature baseFeature : storedFeatures.get()) {
        } else {
            for (CommonFoldingFeature baseFeature : storedFeatures) {
                Integer state = convertToExtensionState(baseFeature.getState());
                if (state == null) {
                    continue;
@@ -205,9 +194,9 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
                    features.add(new FoldingFeature(featureRect, baseFeature.getType(), state));
                }
            }
        }
            return features;
        }
    }

    /**
     * Returns {@link true} if a {@link Rect} has zero width and zero height,
@@ -233,7 +222,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
        private void onDisplayFeaturesChangedIfListening(Activity activity) {
            IBinder token = activity.getWindow().getAttributes().token;
            if (token == null || isListeningForLayoutChanges(token)) {
                onDisplayFeaturesChanged();
                mFoldingFeatureProducer.getData(
                        WindowLayoutComponentImpl.this::onDisplayFeaturesChanged);
            }
        }
    }
+27 −30
Original line number Diff line number Diff line
@@ -28,41 +28,42 @@ import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
import androidx.window.common.RawFoldingFeatureProducer;
import androidx.window.util.DataProducer;
import androidx.window.util.BaseDataProducer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

/**
 * Reference implementation of androidx.window.sidecar OEM interface for use with
 * WindowManager Jetpack.
 */
class SampleSidecarImpl extends StubSidecar {
    private static final String TAG = "SampleSidecar";

    private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer;

    private List<CommonFoldingFeature> mStoredFeatures = new ArrayList<>();

    SampleSidecarImpl(Context context) {
        ((Application) context.getApplicationContext())
                .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged());
        DataProducer<String> settingsFeatureProducer = new RawFoldingFeatureProducer(context);
        mFoldingFeatureProducer = new DeviceStateManagerFoldingFeatureProducer(context,
        BaseDataProducer<String> settingsFeatureProducer = new RawFoldingFeatureProducer(context);
        BaseDataProducer<List<CommonFoldingFeature>> foldingFeatureProducer =
                new DeviceStateManagerFoldingFeatureProducer(context,
                        settingsFeatureProducer);

        mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
        foldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
    }

    private void setStoredFeatures(List<CommonFoldingFeature> storedFeatures) {
        mStoredFeatures = storedFeatures;
    }

    private void onDisplayFeaturesChanged() {
    private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) {
        setStoredFeatures(storedFeatures);
        updateDeviceState(getDeviceState());
        for (IBinder windowToken : getWindowsListeningForLayoutChanges()) {
            SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken);
@@ -79,16 +80,16 @@ class SampleSidecarImpl extends StubSidecar {
    }

    private int deviceStateFromFeature() {
        List<CommonFoldingFeature> storedFeatures = mFoldingFeatureProducer.getData()
                .orElse(Collections.emptyList());
        for (int i = 0; i < storedFeatures.size(); i++) {
            CommonFoldingFeature feature = storedFeatures.get(i);
        for (int i = 0; i < mStoredFeatures.size(); i++) {
            CommonFoldingFeature feature = mStoredFeatures.get(i);
            final int state = feature.getState();
            switch (state) {
                case CommonFoldingFeature.COMMON_STATE_FLAT:
                    return SidecarDeviceState.POSTURE_OPENED;
                case CommonFoldingFeature.COMMON_STATE_HALF_OPENED:
                    return SidecarDeviceState.POSTURE_HALF_OPENED;
                case CommonFoldingFeature.COMMON_STATE_UNKNOWN:
                    return SidecarDeviceState.POSTURE_UNKNOWN;
            }
        }
        return SidecarDeviceState.POSTURE_UNKNOWN;
@@ -109,7 +110,6 @@ class SampleSidecarImpl extends StubSidecar {
    private List<SidecarDisplayFeature> getDisplayFeatures(@NonNull Activity activity) {
        int displayId = activity.getDisplay().getDisplayId();
        if (displayId != DEFAULT_DISPLAY) {
            Log.w(TAG, "This sample doesn't support display features on secondary displays");
            return Collections.emptyList();
        }

@@ -119,10 +119,8 @@ class SampleSidecarImpl extends StubSidecar {
            return Collections.emptyList();
        }

        Optional<List<CommonFoldingFeature>> storedFeatures = mFoldingFeatureProducer.getData();
        List<SidecarDisplayFeature> features = new ArrayList<>();
        if (storedFeatures.isPresent()) {
            for (CommonFoldingFeature baseFeature : storedFeatures.get()) {
        for (CommonFoldingFeature baseFeature : mStoredFeatures) {
            SidecarDisplayFeature feature = new SidecarDisplayFeature();
            Rect featureRect = baseFeature.getRect();
            rotateRectToDisplayRotation(displayId, featureRect);
@@ -131,14 +129,13 @@ class SampleSidecarImpl extends StubSidecar {
            feature.setType(baseFeature.getType());
            features.add(feature);
        }
        }
        return Collections.unmodifiableList(features);
    }

    @Override
    protected void onListenersChanged() {
        if (hasListeners()) {
            onDisplayFeaturesChanged();
            onDisplayFeaturesChanged(mStoredFeatures);
        }
    }

@@ -158,7 +155,7 @@ class SampleSidecarImpl extends StubSidecar {
        private void onDisplayFeaturesChangedForActivity(@NonNull Activity activity) {
            IBinder token = activity.getWindow().getAttributes().token;
            if (token == null || mWindowLayoutChangeListenerTokens.contains(token)) {
                onDisplayFeaturesChanged();
                onDisplayFeaturesChanged(mStoredFeatures);
            }
        }
    }
+42 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 androidx.window.util;

import android.annotation.NonNull;

import java.util.function.Consumer;

/**
 * A base class that works with {@link BaseDataProducer} to add/remove a consumer that should
 * only be used once when {@link BaseDataProducer#notifyDataChanged} is called.
 * @param <T> The type of data this producer returns through {@link DataProducer#getData}.
 */
public class AcceptOnceConsumer<T> implements Consumer<T> {
    private final Consumer<T> mCallback;
    private final DataProducer<T> mProducer;

    public AcceptOnceConsumer(@NonNull DataProducer<T> producer, @NonNull Consumer<T> callback) {
        mProducer = producer;
        mCallback = callback;
    }

    @Override
    public void accept(@NonNull T t) {
        mCallback.accept(t);
        mProducer.removeDataChangedCallback(this);
    }
}
Loading