1/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.launcher3.touch;
17
18import static android.view.MotionEvent.ACTION_CANCEL;
19import static android.view.MotionEvent.ACTION_DOWN;
20import static android.view.MotionEvent.ACTION_POINTER_UP;
21import static android.view.MotionEvent.ACTION_UP;
22import static android.view.ViewConfiguration.getLongPressTimeout;
23
24import static com.android.launcher3.LauncherState.NORMAL;
25
26import android.graphics.PointF;
27import android.graphics.Rect;
28import android.view.HapticFeedbackConstants;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.View.OnTouchListener;
32
33import com.android.launcher3.AbstractFloatingView;
34import com.android.launcher3.CellLayout;
35import com.android.launcher3.DeviceProfile;
36import com.android.launcher3.Launcher;
37import com.android.launcher3.Workspace;
38import com.android.launcher3.dragndrop.DragLayer;
39import com.android.launcher3.views.OptionsPopupView;
40import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
41import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
42
43/**
44 * Helper class to handle touch on empty space in workspace and show options popup on long press
45 */
46public class WorkspaceTouchListener implements OnTouchListener, Runnable {
47
48    /**
49     * STATE_PENDING_PARENT_INFORM is the state between longPress performed & the next motionEvent.
50     * This next event is used to send an ACTION_CANCEL to Workspace, to that it clears any
51     * temporary scroll state. After that, the state is set to COMPLETED, and we just eat up all
52     * subsequent motion events.
53     */
54    private static final int STATE_CANCELLED = 0;
55    private static final int STATE_REQUESTED = 1;
56    private static final int STATE_PENDING_PARENT_INFORM = 2;
57    private static final int STATE_COMPLETED = 3;
58
59    private final Rect mTempRect = new Rect();
60    private final Launcher mLauncher;
61    private final Workspace mWorkspace;
62    private final PointF mTouchDownPoint = new PointF();
63
64    private int mLongPressState = STATE_CANCELLED;
65
66    public WorkspaceTouchListener(Launcher launcher, Workspace workspace) {
67        mLauncher = launcher;
68        mWorkspace = workspace;
69    }
70
71    @Override
72    public boolean onTouch(View view, MotionEvent ev) {
73        int action = ev.getActionMasked();
74        if (action == ACTION_DOWN) {
75            // Check if we can handle long press.
76            boolean handleLongPress = canHandleLongPress();
77
78            if (handleLongPress) {
79                // Check if the event is not near the edges
80                DeviceProfile dp = mLauncher.getDeviceProfile();
81                DragLayer dl = mLauncher.getDragLayer();
82                Rect insets = dp.getInsets();
83
84                mTempRect.set(insets.left, insets.top, dl.getWidth() - insets.right,
85                        dl.getHeight() - insets.bottom);
86                mTempRect.inset(dp.edgeMarginPx, dp.edgeMarginPx);
87                handleLongPress = mTempRect.contains((int) ev.getX(), (int) ev.getY());
88            }
89
90            cancelLongPress();
91            if (handleLongPress) {
92                mLongPressState = STATE_REQUESTED;
93                mTouchDownPoint.set(ev.getX(), ev.getY());
94                mWorkspace.postDelayed(this, getLongPressTimeout());
95            }
96
97            mWorkspace.onTouchEvent(ev);
98            // Return true to keep receiving touch events
99            return true;
100        }
101
102        if (mLongPressState == STATE_PENDING_PARENT_INFORM) {
103            // Inform the workspace to cancel touch handling
104            ev.setAction(ACTION_CANCEL);
105            mWorkspace.onTouchEvent(ev);
106
107            ev.setAction(action);
108            mLongPressState = STATE_COMPLETED;
109        }
110
111        final boolean result;
112        if (mLongPressState == STATE_COMPLETED) {
113            // We have handled the touch, so workspace does not need to know anything anymore.
114            result = true;
115        } else if (mLongPressState == STATE_REQUESTED) {
116            mWorkspace.onTouchEvent(ev);
117            if (mWorkspace.isHandlingTouch()) {
118                cancelLongPress();
119            }
120
121            result = true;
122        } else {
123            // We don't want to handle touch, let workspace handle it as usual.
124            result = false;
125        }
126
127        if (action == ACTION_UP || action == ACTION_POINTER_UP) {
128            if (!mWorkspace.isTouchActive()) {
129                final CellLayout currentPage =
130                        (CellLayout) mWorkspace.getChildAt(mWorkspace.getCurrentPage());
131                if (currentPage != null) {
132                    mWorkspace.onWallpaperTap(ev);
133                }
134            }
135        }
136
137        if (action == ACTION_UP || action == ACTION_CANCEL) {
138            cancelLongPress();
139        }
140        return result;
141    }
142
143    private boolean canHandleLongPress() {
144        return AbstractFloatingView.getTopOpenView(mLauncher) == null
145                && mLauncher.isInState(NORMAL);
146    }
147
148    private void cancelLongPress() {
149        mWorkspace.removeCallbacks(this);
150        mLongPressState = STATE_CANCELLED;
151    }
152
153    @Override
154    public void run() {
155        if (mLongPressState == STATE_REQUESTED) {
156            if (canHandleLongPress()) {
157                mLongPressState = STATE_PENDING_PARENT_INFORM;
158                mWorkspace.getParent().requestDisallowInterceptTouchEvent(true);
159
160                mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
161                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
162                mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
163                        Action.Direction.NONE, ContainerType.WORKSPACE,
164                        mWorkspace.getCurrentPage());
165                OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
166            } else {
167                cancelLongPress();
168            }
169        }
170    }
171}
172