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

Commit 9bdbf292 authored by Bryce Lee's avatar Bryce Lee
Browse files

TouchInsetManager Introduction.

This changelist introduces TouchInsetManager to handle
touchable insets across different locations within the
same root view.

Bug: 214292772
Test: atest TouchInsetManagerTest
Change-Id: I9fa50d47440a130ffd8ed36fd9bc1e4f42ef303c
parent 9492d508
Loading
Loading
Loading
Loading
+1 −44
Original line number Diff line number Diff line
@@ -18,12 +18,9 @@ package com.android.systemui.dreams;

import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;

import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
@@ -62,43 +59,6 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
    // Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates).
    private final Handler mHandler;

    // A hook into the internal inset calculation where we declare the overlays as the only
    // touchable regions.
    private final ViewTreeObserver.OnComputeInternalInsetsListener
            mOnComputeInternalInsetsListener =
            new ViewTreeObserver.OnComputeInternalInsetsListener() {
                @Override
                public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
                    inoutInfo.setTouchableInsets(
                            ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
                    final Region region = new Region();
                    final Rect rect = new Rect();
                    final int childCount = mDreamOverlayContentView.getChildCount();
                    for (int i = 0; i < childCount; i++) {
                        View child = mDreamOverlayContentView.getChildAt(i);

                        if (mComplicationHostViewController.getView() == child) {
                            region.op(mComplicationHostViewController.getTouchRegions(),
                                    Region.Op.UNION);
                            continue;
                        }

                        if (child.getGlobalVisibleRect(rect)) {
                            region.op(rect, Region.Op.UNION);
                        }
                    }

                    // Add the notifications drag area to the tap region (otherwise the
                    // notifications shade can't be dragged down).
                    if (mDreamOverlayContentView.getGlobalVisibleRect(rect)) {
                        rect.bottom = rect.top + mDreamOverlayNotificationsDragAreaHeight;
                        region.op(rect, Region.Op.UNION);
                    }

                    inoutInfo.touchableRegion.set(region);
                }
            };

    @Inject
    public DreamOverlayContainerViewController(
            DreamOverlayContainerView containerView,
@@ -136,16 +96,12 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve

    @Override
    protected void onViewAttached() {
        mView.getViewTreeObserver()
                .addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
        mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval);
    }

    @Override
    protected void onViewDetached() {
        mHandler.removeCallbacks(this::updateBurnInOffsets);
        mView.getViewTreeObserver()
                .removeOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
    }

    View getContainerView() {
@@ -162,6 +118,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
        // so no translation occurs when the values don't change.
        mView.setTranslationX(getBurnInOffset(mMaxBurnInOffset * 2, true)
                - mMaxBurnInOffset);

        mView.setTranslationY(getBurnInOffset(mMaxBurnInOffset * 2, false)
                - mMaxBurnInOffset);

+7 −1
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.net.NetworkCapabilities;
import android.net.NetworkRequest;

import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.ViewController;

import java.lang.annotation.Retention;
@@ -48,6 +49,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
    private static final int WIFI_STATUS_AVAILABLE = 2;

    private final ConnectivityManager mConnectivityManager;
    private final TouchInsetManager.TouchInsetSession mTouchInsetSession;

    private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder()
            .clearCapabilities()
@@ -77,9 +79,11 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
    @Inject
    public DreamOverlayStatusBarViewController(
            DreamOverlayStatusBarView view,
            ConnectivityManager connectivityManager) {
            ConnectivityManager connectivityManager,
            TouchInsetManager.TouchInsetSession touchInsetSession) {
        super(view);
        mConnectivityManager = connectivityManager;
        mTouchInsetSession = touchInsetSession;
    }

    @Override
@@ -92,11 +96,13 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
        onWifiAvailabilityChanged(
                capabilities != null
                        && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI));
        mTouchInsetSession.addViewToTracking(mView);
    }

    @Override
    protected void onViewDetached() {
        mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
        mTouchInsetSession.clear();
    }

    /**
+19 −5
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Constraints;

import com.android.systemui.R;
import com.android.systemui.touch.TouchInsetManager;

import java.util.ArrayList;
import java.util.Collections;
@@ -52,6 +53,7 @@ public class ComplicationLayoutEngine {
    private static class ViewEntry implements Comparable<ViewEntry> {
        private final View mView;
        private final ComplicationLayoutParams mLayoutParams;
        private final TouchInsetManager.TouchInsetSession mTouchInsetSession;
        private final Parent mParent;
        @Complication.Category
        private final int mCategory;
@@ -61,7 +63,8 @@ public class ComplicationLayoutEngine {
         * Default constructor. {@link Parent} allows for the {@link ViewEntry}'s surrounding
         * view hierarchy to be accessed without traversing the entire view tree.
         */
        ViewEntry(View view, ComplicationLayoutParams layoutParams, int category, Parent parent,
        ViewEntry(View view, ComplicationLayoutParams layoutParams,
                TouchInsetManager.TouchInsetSession touchSession, int category, Parent parent,
                int margin) {
            mView = view;
            // Views that are generated programmatically do not have a unique id assigned to them
@@ -70,9 +73,12 @@ public class ComplicationLayoutEngine {
            // {@link Complication.ViewHolder} should not reference the root container by id.
            mView.setId(View.generateViewId());
            mLayoutParams = layoutParams;
            mTouchInsetSession = touchSession;
            mCategory = category;
            mParent = parent;
            mMargin = margin;

            touchSession.addViewToTracking(mView);
        }

        /**
@@ -217,6 +223,7 @@ public class ComplicationLayoutEngine {
            mParent.removeEntry(this);

            ((ViewGroup) mView.getParent()).removeView(mView);
            mTouchInsetSession.removeViewFromTracking(mView);
        }

        @Override
@@ -242,15 +249,18 @@ public class ComplicationLayoutEngine {
         */
        private static class Builder {
            private final View mView;
            private final TouchInsetManager.TouchInsetSession mTouchSession;
            private final ComplicationLayoutParams mLayoutParams;
            private final int mCategory;
            private Parent mParent;
            private int mMargin;

            Builder(View view, ComplicationLayoutParams lp, @Complication.Category int category) {
            Builder(View view, TouchInsetManager.TouchInsetSession touchSession,
                    ComplicationLayoutParams lp, @Complication.Category int category) {
                mView = view;
                mLayoutParams = lp;
                mCategory = category;
                mTouchSession = touchSession;
            }

            /**
@@ -291,7 +301,8 @@ public class ComplicationLayoutEngine {
             * Builds and returns the resulting {@link ViewEntry}.
             */
            ViewEntry build() {
                return new ViewEntry(mView, mLayoutParams, mCategory, mParent, mMargin);
                return new ViewEntry(mView, mLayoutParams, mTouchSession, mCategory, mParent,
                        mMargin);
            }
        }

@@ -442,13 +453,16 @@ public class ComplicationLayoutEngine {
    private final int mMargin;
    private final HashMap<ComplicationId, ViewEntry> mEntries = new HashMap<>();
    private final HashMap<Integer, PositionGroup> mPositions = new HashMap<>();
    private final TouchInsetManager.TouchInsetSession mSession;

    /** */
    @Inject
    public ComplicationLayoutEngine(@Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout,
            @Named(COMPLICATION_MARGIN) int margin) {
            @Named(COMPLICATION_MARGIN) int margin,
            TouchInsetManager.TouchInsetSession session) {
        mLayout = layout;
        mMargin = margin;
        mSession = session;
    }

    /**
@@ -468,7 +482,7 @@ public class ComplicationLayoutEngine {
            removeComplication(id);
        }

        final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, lp, category)
        final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, mSession, lp, category)
                .setMargin(mMargin);

        // Add position group if doesn't already exist
+18 −0
Original line number Diff line number Diff line
@@ -29,6 +29,9 @@ import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.DreamOverlayContainerView;
import com.android.systemui.dreams.DreamOverlayStatusBarView;
import com.android.systemui.touch.TouchInsetManager;

import java.util.concurrent.Executor;

import javax.inject.Named;

@@ -63,6 +66,21 @@ public abstract class DreamOverlayModule {
                "R.id.dream_overlay_content must not be null");
    }

    /** */
    @Provides
    public static TouchInsetManager.TouchInsetSession providesTouchInsetSession(
            TouchInsetManager manager) {
        return manager.createSession();
    }

    /** */
    @Provides
    @DreamOverlayComponent.DreamOverlayScope
    public static TouchInsetManager providesTouchInsetManager(@Main Executor executor,
            DreamOverlayContainerView view) {
        return new TouchInsetManager(executor, view);
    }

    /** */
    @Provides
    @DreamOverlayComponent.DreamOverlayScope
+181 −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 com.android.systemui.touch;

import android.graphics.Rect;
import android.graphics.Region;
import android.view.View;
import android.view.ViewRootImpl;

import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.Executor;

/**
 * {@link TouchInsetManager} handles setting the touchable inset regions for a given View. This
 * is useful for passing through touch events for all but select areas.
 */
public class TouchInsetManager {
    /**
     * {@link TouchInsetSession} provides an individualized session with the
     * {@link TouchInsetManager}, linking any action to the client.
     */
    public static class TouchInsetSession {
        private final TouchInsetManager mManager;

        private final HashSet<View> mTrackedViews;
        private final Executor mExecutor;

        private final View.OnLayoutChangeListener mOnLayoutChangeListener =
                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
                        -> updateTouchRegion();

        /**
         * Default constructor
         * @param manager The parent {@link TouchInsetManager} which will be affected by actions on
         *                this session.
         * @param rootView The parent of views that will be tracked.
         * @param executor An executor for marshalling operations.
         */
        TouchInsetSession(TouchInsetManager manager, Executor executor) {
            mManager = manager;
            mTrackedViews = new HashSet<>();
            mExecutor = executor;
        }

        /**
         * Adds a descendant of the root view to be tracked.
         * @param view {@link View} to be tracked.
         */
        public void addViewToTracking(View view) {
            mExecutor.execute(() -> {
                mTrackedViews.add(view);
                view.addOnLayoutChangeListener(mOnLayoutChangeListener);
                updateTouchRegion();
            });
        }

        /**
         * Removes a view from further tracking
         * @param view {@link View} to be removed.
         */
        public void removeViewFromTracking(View view) {
            mExecutor.execute(() -> {
                mTrackedViews.remove(view);
                view.removeOnLayoutChangeListener(mOnLayoutChangeListener);
                updateTouchRegion();
            });
        }

        private void updateTouchRegion() {
            final Region cumulativeRegion = Region.obtain();

            mTrackedViews.stream().forEach(view -> {
                final Rect boundaries = new Rect();
                view.getBoundsOnScreen(boundaries);
                cumulativeRegion.op(boundaries, Region.Op.UNION);
            });

            mManager.setTouchRegion(this, cumulativeRegion);

            cumulativeRegion.recycle();
        }

        /**
         * Removes all tracked views and updates insets accordingly.
         */
        public void clear() {
            mExecutor.execute(() -> {
                mManager.clearRegion(this);
                mTrackedViews.clear();
            });
        }
    }

    private final HashMap<TouchInsetSession, Region> mDefinedRegions = new HashMap<>();
    private final Executor mExecutor;
    private final View mRootView;

    private final View.OnAttachStateChangeListener mAttachListener =
            new View.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    updateTouchInset();
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                }
            };

    /**
     * Default constructor.
     * @param executor An {@link Executor} to marshal all operations on.
     * @param rootView The root {@link View} for all views in sessions.
     */
    public TouchInsetManager(Executor executor, View rootView) {
        mExecutor = executor;
        mRootView = rootView;
        mRootView.addOnAttachStateChangeListener(mAttachListener);

    }

    /**
     * Creates a new associated session.
     */
    public TouchInsetSession createSession() {
        return new TouchInsetSession(this, mExecutor);
    }

    private void updateTouchInset() {
        final ViewRootImpl viewRootImpl = mRootView.getViewRootImpl();

        if (viewRootImpl == null) {
            return;
        }

        final Region aggregateRegion = Region.obtain();

        for (Region region : mDefinedRegions.values()) {
            aggregateRegion.op(region, Region.Op.UNION);
        }

        viewRootImpl.setTouchableRegion(aggregateRegion);

        aggregateRegion.recycle();
    }

    protected void setTouchRegion(TouchInsetSession session, Region region) {
        final Region introducedRegion = Region.obtain(region);
        mExecutor.execute(() -> {
            mDefinedRegions.put(session, introducedRegion);
            updateTouchInset();
        });
    }

    private void clearRegion(TouchInsetSession session) {
        mExecutor.execute(() -> {
            final Region storedRegion = mDefinedRegions.remove(session);

            if (storedRegion != null) {
                storedRegion.recycle();
            }

            updateTouchInset();
        });
    }
}
Loading