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

Commit b55d5b27 authored by Andy Wickham's avatar Andy Wickham Committed by Android (Google) Code Review
Browse files

Merge "Adds back gesture recognition to Sandbox." into ub-launcher3-master

parents 925639d8 3e34dd37
Loading
Loading
Loading
Loading
+0 −2
Original line number Diff line number Diff line
@@ -16,9 +16,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layerType="software"
    android:background="@color/back_gesture_tutorial_background_color">
    <!--The layout is rendered on the software layer to avoid b/136158117-->

    <ImageView
        android:id="@+id/back_gesture_tutorial_fragment_hand_coaching"
+18 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
    <color name="back_arrow_color_dark">#99000000</color>
</resources>
 No newline at end of file
+21 −8
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.widget.TextView;
import com.android.launcher3.R;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;

import java.util.Optional;

@@ -79,22 +80,34 @@ abstract class BackGestureTutorialController {
        mHandCoachingAnimation.stop();
    }

    void onGestureDetected() {
        hideHandCoachingAnimation();

        if (mTutorialStep == TutorialStep.CONFIRM) {
    void onGestureAttempted(BackGestureResult result) {
        if (mTutorialStep == TutorialStep.CONFIRM
                && (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
                    || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT)) {
            mFragment.closeTutorial();
            return;
        }

        if (mTutorialTypeInfo.get().getTutorialType() == TutorialType.RIGHT_EDGE_BACK_NAVIGATION) {
            mFragment.changeController(TutorialStep.ENGAGED,
                    TutorialType.LEFT_EDGE_BACK_NAVIGATION);
        if (!mTutorialTypeInfo.isPresent()) {
            return;
        }

        switch (mTutorialTypeInfo.get().getTutorialType()) {
            case RIGHT_EDGE_BACK_NAVIGATION:
                if (result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
                    hideHandCoachingAnimation();
                    mFragment.changeController(
                            TutorialStep.ENGAGED, TutorialType.LEFT_EDGE_BACK_NAVIGATION);
                }
                break;
            case LEFT_EDGE_BACK_NAVIGATION:
                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT) {
                    hideHandCoachingAnimation();
                    mFragment.changeController(TutorialStep.CONFIRM);
                }
                break;
        }
    }

    abstract Optional<Integer> getTitleStringId();

+27 −6
Original line number Diff line number Diff line
@@ -17,21 +17,26 @@ package com.android.quickstep.interaction;

import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.graphics.Insets;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;

import com.android.launcher3.R;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;

import java.net.URISyntaxException;
import java.util.Optional;

/** Shows the Back gesture interactive tutorial. */
public class BackGestureTutorialFragment extends Fragment {
public class BackGestureTutorialFragment extends Fragment implements BackGestureAttemptCallback {

    private static final String LOG_TAG = "TutorialFragment";
    private static final String KEY_TUTORIAL_STEP = "tutorialStep";
@@ -47,6 +52,7 @@ public class BackGestureTutorialFragment extends Fragment {
    private Optional<BackGestureTutorialController> mTutorialController = Optional.empty();
    private View mRootView;
    private BackGestureTutorialHandAnimation mHandCoachingAnimation;
    private EdgeBackGestureHandler mEdgeBackGestureHandler;

    public static BackGestureTutorialFragment newInstance(
            TutorialStep tutorialStep, TutorialType tutorialType) {
@@ -64,17 +70,25 @@ public class BackGestureTutorialFragment extends Fragment {
        Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
        mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP);
        mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
        mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
        mEdgeBackGestureHandler.registerBackGestureAttemptCallback(this);
    }

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);

        mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment,
                container, /* attachToRoot= */ false);
        mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button)
                .setOnClickListener(this::onCloseButtonClicked);
        mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
            Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
            mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
            return insets;
        });
        mRootView.setOnTouchListener(mEdgeBackGestureHandler);
        mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView);

        return mRootView;
@@ -92,6 +106,14 @@ public class BackGestureTutorialFragment extends Fragment {
        mHandCoachingAnimation.stop();
    }

    void onAttachedToWindow() {
        mEdgeBackGestureHandler.setIsEnabled(true);
    }

    void onDetachedFromWindow() {
        mEdgeBackGestureHandler.setIsEnabled(false);
    }

    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
        savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep);
@@ -125,10 +147,9 @@ public class BackGestureTutorialFragment extends Fragment {
        this.mTutorialType = tutorialType;
    }

    void onBackPressed() {
        if (mTutorialController.isPresent()) {
            mTutorialController.get().onGestureDetected();
        }
    @Override
    public void onBackGestureAttempted(BackGestureResult result) {
        mTutorialController.ifPresent(controller -> controller.onGestureAttempted(result));
    }

    void closeTutorial() {
+274 −0
Original line number 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.quickstep.interaction;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.PointF;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemProperties;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;

import com.android.launcher3.ResourceUtils;

/**
 * Utility class to handle edge swipes for back gestures.
 *
 * Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java.
 */
public class EdgeBackGestureHandler implements DisplayListener, OnTouchListener {

    private static final String TAG = "EdgeBackGestureHandler";
    private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt(
            "gestures.back_timeout", 250);

    private final Context mContext;

    private final Point mDisplaySize = new Point();
    private final int mDisplayId;

    // The edge width where touch down is allowed
    private int mEdgeWidth;
    // The bottom gesture area height
    private int mBottomGestureHeight;
    // The slop to distinguish between horizontal and vertical motion
    private final float mTouchSlop;
    // Duration after which we consider the event as longpress.
    private final int mLongPressTimeout;

    private final PointF mDownPoint = new PointF();
    private boolean mThresholdCrossed = false;
    private boolean mAllowGesture = false;
    private boolean mIsEnabled;
    private int mLeftInset;
    private int mRightInset;

    private EdgeBackGesturePanel mEdgeBackPanel;
    private BackGestureAttemptCallback mGestureCallback;

    private final EdgeBackGesturePanel.BackCallback mBackCallback =
            new EdgeBackGesturePanel.BackCallback() {
                @Override
                public void triggerBack() {
                    if (mGestureCallback != null) {
                        mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
                                ? BackGestureResult.BACK_COMPLETED_FROM_LEFT
                                : BackGestureResult.BACK_COMPLETED_FROM_RIGHT);
                    }
                }

                @Override
                public void cancelBack() {
                    if (mGestureCallback != null) {
                        mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
                                ? BackGestureResult.BACK_CANCELLED_FROM_LEFT
                                : BackGestureResult.BACK_CANCELLED_FROM_RIGHT);
                    }
                }
            };

    EdgeBackGestureHandler(Context context) {
        final Resources res = context.getResources();
        mContext = context;
        mDisplayId = context.getDisplay() == null
                ? Display.DEFAULT_DISPLAY : context.getDisplay().getDisplayId();

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT,
                ViewConfiguration.getLongPressTimeout());

        mBottomGestureHeight =
            ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, res);
        mEdgeWidth = ResourceUtils.getNavbarSize("config_backGestureInset", res);
    }

    void setIsEnabled(boolean isEnabled) {
        if (isEnabled == mIsEnabled) {
            return;
        }
        mIsEnabled = isEnabled;

        if (mEdgeBackPanel != null) {
            mEdgeBackPanel.onDestroy();
            mEdgeBackPanel = null;
        }

        if (!mIsEnabled) {
            mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
        } else {
            updateDisplaySize();
            mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
                    new Handler(Looper.getMainLooper()));

            // Add a nav bar panel window.
            mEdgeBackPanel = new EdgeBackGesturePanel(mContext);
            mEdgeBackPanel.setBackCallback(mBackCallback);
            mEdgeBackPanel.setLayoutParams(createLayoutParams());
            updateDisplaySize();
        }
    }

    void registerBackGestureAttemptCallback(BackGestureAttemptCallback callback) {
        mGestureCallback = callback;
    }

    private WindowManager.LayoutParams createLayoutParams() {
        Resources resources = mContext.getResources();
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                ResourceUtils.getNavbarSize("navigation_edge_panel_width", resources),
                ResourceUtils.getNavbarSize("navigation_edge_panel_height", resources),
                LayoutParams.TYPE_APPLICATION_PANEL,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
                        | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
                PixelFormat.TRANSLUCENT);
        layoutParams.setTitle(TAG + mDisplayId);
        layoutParams.windowAnimations = 0;
        layoutParams.setFitInsetsTypes(0 /* types */);
        return layoutParams;
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        if (mIsEnabled) {
            onMotionEvent(motionEvent);
            return true;
        }
        return false;
    }

    private boolean isWithinTouchRegion(int x, int y) {
        // Disallow if too far from the edge
        if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) {
            return false;
        }

        // Disallow if we are in the bottom gesture area
        if (y >= (mDisplaySize.y - mBottomGestureHeight)) {
            return false;
        }

        return true;
    }

    private void cancelGesture(MotionEvent ev) {
        // Send action cancel to reset all the touch events
        mAllowGesture = false;
        MotionEvent cancelEv = MotionEvent.obtain(ev);
        cancelEv.setAction(MotionEvent.ACTION_CANCEL);
        mEdgeBackPanel.onMotionEvent(cancelEv);
        cancelEv.recycle();
    }

    private void onMotionEvent(MotionEvent ev) {
        int action = ev.getActionMasked();
        if (action == MotionEvent.ACTION_DOWN) {
            boolean isOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset;
            mAllowGesture = isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
            if (mAllowGesture) {
                mEdgeBackPanel.setIsLeftPanel(isOnLeftEdge);
                mEdgeBackPanel.onMotionEvent(ev);

                mDownPoint.set(ev.getX(), ev.getY());
                mThresholdCrossed = false;
            }
        } else if (mAllowGesture) {
            if (!mThresholdCrossed) {
                if (action == MotionEvent.ACTION_POINTER_DOWN) {
                    // We do not support multi touch for back gesture
                    cancelGesture(ev);
                    return;
                } else if (action == MotionEvent.ACTION_MOVE) {
                    if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) {
                        cancelGesture(ev);
                        return;
                    }
                    float dx = Math.abs(ev.getX() - mDownPoint.x);
                    float dy = Math.abs(ev.getY() - mDownPoint.y);
                    if (dy > dx && dy > mTouchSlop) {
                        cancelGesture(ev);
                        return;

                    } else if (dx > dy && dx > mTouchSlop) {
                        mThresholdCrossed = true;
                    }
                }

            }

            // forward touch
            mEdgeBackPanel.onMotionEvent(ev);
        }

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            if (!mAllowGesture && mGestureCallback != null) {
                mGestureCallback.onBackGestureAttempted(BackGestureResult.BACK_NOT_STARTED);
            }
        }
    }

    @Override
    public void onDisplayAdded(int displayId) { }

    @Override
    public void onDisplayRemoved(int displayId) { }

    @Override
    public void onDisplayChanged(int displayId) {
        if (displayId == mDisplayId) {
            updateDisplaySize();
        }
    }

    private void updateDisplaySize() {
        mContext.getDisplay().getRealSize(mDisplaySize);
        if (mEdgeBackPanel != null) {
            mEdgeBackPanel.setDisplaySize(mDisplaySize);
        }
    }

    void setInsets(int leftInset, int rightInset) {
        mLeftInset = leftInset;
        mRightInset = rightInset;
    }

    enum BackGestureResult {
        UNKNOWN,
        BACK_COMPLETED_FROM_LEFT,
        BACK_COMPLETED_FROM_RIGHT,
        BACK_CANCELLED_FROM_LEFT,
        BACK_CANCELLED_FROM_RIGHT,
        BACK_NOT_STARTED,
    }

    /** Callback to let the UI react to attempted back gestures. */
    interface BackGestureAttemptCallback {
        /** Called whenever any touch is completed. */
        void onBackGestureAttempted(BackGestureResult result);
    }
}
Loading