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

Commit 8ee9dd3b authored by Charles Chen's avatar Charles Chen
Browse files

Add WindowContext support to WindowLayoutComponentImpl

To make activity embedding library able to listen to folding state
change consistently, we should add WindowContext support to
WindowLayoutComponentImpl, which only supported Activity previously.

This CL can also benefit VoiceInteractionSession support on
WindowLayoutComponent.

Bug: 207494880
Bug: 204073440
Test: use in follow-up CL and report folding state change successfully

Change-Id: I7a4fea6ed00b52996c7cffb33aadb934abe811c8
parent 39276b1f
Loading
Loading
Loading
Loading
+102 −40
Original line number Diff line number Diff line
@@ -20,21 +20,26 @@ import static android.view.Display.DEFAULT_DISPLAY;

import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
import static androidx.window.util.ExtensionHelper.isZero;
import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;

import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityClient;
import android.app.Application;
import android.app.WindowConfiguration;
import android.content.ComponentCallbacks;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
import android.util.ArrayMap;
import android.window.WindowContext;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiContext;
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
@@ -58,11 +63,14 @@ import java.util.function.Consumer;
public class WindowLayoutComponentImpl implements WindowLayoutComponent {
    private static final String TAG = "SampleExtension";

    private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners =
    private final Map<Context, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners =
            new ArrayMap<>();

    private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer;

    private final Map<IBinder, WindowContextConfigListener> mWindowContextConfigListeners =
            new ArrayMap<>();

    public WindowLayoutComponentImpl(@NonNull Context context) {
        ((Application) context.getApplicationContext())
                .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged());
@@ -78,14 +86,42 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
     * @param activity hosting a {@link android.view.Window}
     * @param consumer interested in receiving updates to {@link WindowLayoutInfo}
     */
    @Override
    public void addWindowLayoutInfoListener(@NonNull Activity activity,
            @NonNull Consumer<WindowLayoutInfo> consumer) {
        addWindowLayoutInfoListener((Context) activity, consumer);
    }

    /**
     * Similar to {@link #addWindowLayoutInfoListener(Activity, Consumer)}, but takes a UI Context
     * as a parameter.
     */
    // TODO(b/204073440): Add @Override to hook the API in WM extensions library.
    public void addWindowLayoutInfoListener(@NonNull @UiContext Context context,
            @NonNull Consumer<WindowLayoutInfo> consumer) {
        if (mWindowLayoutChangeListeners.containsKey(context)
                || mWindowLayoutChangeListeners.containsValue(consumer)) {
            // Early return if the listener or consumer has been registered.
            return;
        }
        if (!context.isUiContext()) {
            throw new IllegalArgumentException("Context must be a UI Context, which should be"
                    + " an Activity or a WindowContext");
        }
        mFoldingFeatureProducer.getData((features) -> {
            // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer.
            WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(activity, features);
            WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features);
            consumer.accept(newWindowLayout);
        });
        mWindowLayoutChangeListeners.put(activity, consumer);
        mWindowLayoutChangeListeners.put(context, consumer);

        if (context instanceof WindowContext) {
            final IBinder windowContextToken = context.getWindowContextToken();
            final WindowContextConfigListener listener =
                    new WindowContextConfigListener(windowContextToken);
            context.registerComponentCallbacks(listener);
            mWindowContextConfigListeners.put(windowContextToken, listener);
        }
    }

    /**
@@ -93,18 +129,30 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
     *
     * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo}
     */
    @Override
    public void removeWindowLayoutInfoListener(@NonNull Consumer<WindowLayoutInfo> consumer) {
        for (Context context : mWindowLayoutChangeListeners.keySet()) {
            if (!mWindowLayoutChangeListeners.get(context).equals(consumer)) {
                continue;
            }
            if (context instanceof WindowContext) {
                final IBinder token = context.getWindowContextToken();
                context.unregisterComponentCallbacks(mWindowContextConfigListeners.get(token));
                mWindowContextConfigListeners.remove(token);
            }
            break;
        }
        mWindowLayoutChangeListeners.values().remove(consumer);
    }

    @NonNull
    Set<Activity> getActivitiesListeningForLayoutChanges() {
    Set<Context> getContextsListeningForLayoutChanges() {
        return mWindowLayoutChangeListeners.keySet();
    }

    private boolean isListeningForLayoutChanges(IBinder token) {
        for (Activity activity: getActivitiesListeningForLayoutChanges()) {
            if (token.equals(activity.getWindow().getAttributes().token)) {
        for (Context context: getContextsListeningForLayoutChanges()) {
            if (token.equals(Context.getToken(context))) {
                return true;
            }
        }
@@ -138,10 +186,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
    }

    private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) {
        for (Activity activity : getActivitiesListeningForLayoutChanges()) {
        for (Context context : getContextsListeningForLayoutChanges()) {
            // Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer.
            Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(activity);
            WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(activity, storedFeatures);
            Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(context);
            WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, storedFeatures);
            layoutConsumer.accept(newWindowLayout);
        }
    }
@@ -149,11 +197,12 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
    /**
     * 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
     * @param context a proxy for the {@link android.view.Window} that contains the
     * {@link DisplayFeature}.
     */
    private WindowLayoutInfo getWindowLayoutInfo(
            @NonNull Activity activity, List<CommonFoldingFeature> storedFeatures) {
        List<DisplayFeature> displayFeatureList = getDisplayFeatures(activity, storedFeatures);
    private WindowLayoutInfo getWindowLayoutInfo(@NonNull @UiContext Context context,
            List<CommonFoldingFeature> storedFeatures) {
        List<DisplayFeature> displayFeatureList = getDisplayFeatures(context, storedFeatures);
        return new WindowLayoutInfo(displayFeatureList);
    }

@@ -170,18 +219,18 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
     * bounds are not valid, constructing a {@link FoldingFeature} will throw an
     * {@link IllegalArgumentException} since this can cause negative UI effects down stream.
     *
     * @param activity a proxy for the {@link android.view.Window} that contains the
     * @param context a proxy for the {@link android.view.Window} that contains the
     * {@link DisplayFeature}.
     * are within the {@link android.view.Window} of the {@link Activity}
     */
    private List<DisplayFeature> getDisplayFeatures(
            @NonNull Activity activity, List<CommonFoldingFeature> storedFeatures) {
            @NonNull @UiContext Context context, List<CommonFoldingFeature> storedFeatures) {
        List<DisplayFeature> features = new ArrayList<>();
        if (!shouldReportDisplayFeatures(activity)) {
        if (!shouldReportDisplayFeatures(context)) {
            return features;
        }

        int displayId = activity.getDisplay().getDisplayId();
        int displayId = context.getDisplay().getDisplayId();
        for (CommonFoldingFeature baseFeature : storedFeatures) {
            Integer state = convertToExtensionState(baseFeature.getState());
            if (state == null) {
@@ -189,9 +238,9 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
            }
            Rect featureRect = baseFeature.getRect();
            rotateRectToDisplayRotation(displayId, featureRect);
            transformToWindowSpaceRect(activity, featureRect);
            transformToWindowSpaceRect(context, featureRect);

            if (!isRectZero(featureRect)) {
            if (!isZero(featureRect)) {
                // TODO(b/228641877): Remove guarding when fixed.
                features.add(new FoldingFeature(featureRect, baseFeature.getType(), state));
            }
@@ -203,15 +252,21 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
     * Checks whether display features should be reported for the activity.
     * TODO(b/238948678): Support reporting display features in all windowing modes.
     */
    private boolean shouldReportDisplayFeatures(@NonNull Activity activity) {
        int displayId = activity.getDisplay().getDisplayId();
    private boolean shouldReportDisplayFeatures(@NonNull @UiContext Context context) {
        int displayId = context.getDisplay().getDisplayId();
        if (displayId != DEFAULT_DISPLAY) {
            // Display features are not supported on secondary displays.
            return false;
        }
        final int taskWindowingMode = ActivityClient.getInstance().getTaskWindowingMode(
                activity.getActivityToken());
        if (taskWindowingMode == -1) {
        final int windowingMode;
        if (context instanceof Activity) {
            windowingMode = ActivityClient.getInstance().getTaskWindowingMode(
                    context.getActivityToken());
        } else {
            windowingMode = context.getResources().getConfiguration().windowConfiguration
                    .getWindowingMode();
        }
        if (windowingMode == -1) {
            // If we cannot determine the task windowing mode for any reason, it is likely that we
            // won't be able to determine its position correctly as well. DisplayFeatures' bounds
            // in this case can't be computed correctly, so we should skip.
@@ -219,36 +274,43 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
        }
        // 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 !WindowConfiguration.inMultiWindowMode(taskWindowingMode);
        return !WindowConfiguration.inMultiWindowMode(windowingMode);
    }

    /**
     * Returns {@link true} if a {@link Rect} has zero width and zero height,
     * {@code false} otherwise.
     */
    private boolean isRectZero(Rect rect) {
        return rect.width() == 0 && rect.height() == 0;
    private void onDisplayFeaturesChangedIfListening(@NonNull IBinder token) {
        if (isListeningForLayoutChanges(token)) {
            mFoldingFeatureProducer.getData(
                    WindowLayoutComponentImpl.this::onDisplayFeaturesChanged);
        }
    }

    private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter {
        @Override
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            super.onActivityCreated(activity, savedInstanceState);
            onDisplayFeaturesChangedIfListening(activity);
            onDisplayFeaturesChangedIfListening(activity.getActivityToken());
        }

        @Override
        public void onActivityConfigurationChanged(Activity activity) {
            super.onActivityConfigurationChanged(activity);
            onDisplayFeaturesChangedIfListening(activity);
            onDisplayFeaturesChangedIfListening(activity.getActivityToken());
        }
    }

        private void onDisplayFeaturesChangedIfListening(Activity activity) {
            IBinder token = activity.getWindow().getAttributes().token;
            if (token == null || isListeningForLayoutChanges(token)) {
                mFoldingFeatureProducer.getData(
                        WindowLayoutComponentImpl.this::onDisplayFeaturesChanged);
    private final class WindowContextConfigListener implements ComponentCallbacks {
        final IBinder mToken;

        WindowContextConfigListener(IBinder token) {
            mToken = token;
        }

        @Override
        public void onConfigurationChanged(@NonNull Configuration newConfig) {
            onDisplayFeaturesChangedIfListening(mToken);
        }

        @Override
        public void onLowMemory() {}
    }
}
+9 −11
Original line number Diff line number Diff line
@@ -21,14 +21,15 @@ import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;

import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.view.DisplayInfo;
import android.view.Surface;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiContext;

/**
 * Util class for both Sidecar and Extensions.
@@ -86,12 +87,9 @@ public final class ExtensionHelper {
    }

    /** Transforms rectangle from absolute coordinate space to the window coordinate space. */
    public static void transformToWindowSpaceRect(Activity activity, Rect inOutRect) {
        Rect windowRect = getWindowBounds(activity);
        if (windowRect == null) {
            inOutRect.setEmpty();
            return;
        }
    public static void transformToWindowSpaceRect(@NonNull @UiContext Context context,
            Rect inOutRect) {
        Rect windowRect = getWindowBounds(context);
        if (!Rect.intersects(inOutRect, windowRect)) {
            inOutRect.setEmpty();
            return;
@@ -103,9 +101,9 @@ public final class ExtensionHelper {
    /**
     * Gets the current window bounds in absolute coordinates.
     */
    @Nullable
    private static Rect getWindowBounds(@NonNull Activity activity) {
        return activity.getWindowManager().getCurrentWindowMetrics().getBounds();
    @NonNull
    private static Rect getWindowBounds(@NonNull @UiContext Context context) {
        return context.getSystemService(WindowManager.class).getCurrentWindowMetrics().getBounds();
    }

    /**