From 24b209c65a953f6e906dd6f36da9a6c790bf196a Mon Sep 17 00:00:00 2001 From: Hyunyoung Song Date: Mon, 10 Sep 2018 16:48:47 -0700 Subject: [PATCH] Swipe down on status bar Bug: 111839343 Change-Id: I5332dc098af980b4d4ef45b095586d68975ad98c --- .../uioverrides/StatusBarTouchController.java | 115 ++++++++ .../launcher3/uioverrides/UiFactory.java | 31 +- .../android/launcher3/config/BaseFlags.java | 4 + .../AbstractStateChangeTouchController.java | 2 +- .../launcher3/touch/TouchEventTranslator.java | 274 ++++++++++++++++++ 5 files changed, 411 insertions(+), 15 deletions(-) create mode 100644 quickstep/src/com/android/launcher3/uioverrides/StatusBarTouchController.java create mode 100644 src/com/android/launcher3/touch/TouchEventTranslator.java diff --git a/quickstep/src/com/android/launcher3/uioverrides/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/StatusBarTouchController.java new file mode 100644 index 0000000000..d5f6b940ff --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/StatusBarTouchController.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 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.launcher3.uioverrides; + +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; + +import android.os.RemoteException; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.touch.TouchEventTranslator; +import com.android.launcher3.util.TouchController; +import com.android.quickstep.RecentsModel; +import com.android.systemui.shared.recents.ISystemUiProxy; + +/** + * TouchController for handling touch events that get sent to the StatusBar. Once the + * Once the event delta y passes the touch slop, the events start getting forwarded. + * All events are offset by initial Y value of the pointer. + */ +public class StatusBarTouchController implements TouchController { + + private static final String TAG = "StatusBarController"; + + protected final Launcher mLauncher; + protected final TouchEventTranslator mTranslator; + private final float mTouchSlop; + private ISystemUiProxy mSysUiProxy; + + /* If {@code false}, this controller should not handle the input {@link MotionEvent}.*/ + private boolean mCanIntercept; + + public StatusBarTouchController(Launcher l) { + mLauncher = l; + mTouchSlop = ViewConfiguration.get(l).getScaledTouchSlop(); + mTranslator = new TouchEventTranslator((MotionEvent ev)-> dispatchTouchEvent(ev)); + } + + private void dispatchTouchEvent(MotionEvent ev) { + try { + if (mSysUiProxy != null) { + mSysUiProxy.onStatusBarMotionEvent(ev); + } + } catch (RemoteException e) { + Log.e(TAG, "Remote exception on sysUiProxy.", e); + } + } + + @Override + public final boolean onControllerInterceptTouchEvent(MotionEvent ev) { + int action = ev.getActionMasked(); + if (action == ACTION_DOWN) { + mCanIntercept = canInterceptTouch(ev); + if (!mCanIntercept) { + return false; + } + mTranslator.reset(); + mTranslator.setDownParameters(0, ev); + } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { + // Check!! should only set it only when threshold is not entered. + mTranslator.setDownParameters(ev.getActionIndex(), ev); + } + if (!mCanIntercept) { + return false; + } + if (action == ACTION_MOVE) { + float dy = ev.getY() - mTranslator.getDownY(); + if (dy > mTouchSlop) { + mTranslator.dispatchDownEvents(ev); + mTranslator.processMotionEvent(ev); + return true; + } + } + return false; + } + + + @Override + public final boolean onControllerTouchEvent(MotionEvent ev) { + mTranslator.processMotionEvent(ev); + return true; + } + + private boolean canInterceptTouch(MotionEvent ev) { + if (!mLauncher.isInState(LauncherState.NORMAL)) { + return false; + } else { + // For NORMAL state, only listen if the event originated above the navbar height + DeviceProfile dp = mLauncher.getDeviceProfile(); + if (ev.getY() > (mLauncher.getDragLayer().getHeight() - dp.getInsets().bottom)) { + return false; + } + } + mSysUiProxy = RecentsModel.INSTANCE.get(mLauncher).getSystemUiProxy(); + return mSysUiProxy != null; + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java index c9393306f4..5980d85c0b 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java +++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java @@ -42,6 +42,7 @@ import com.android.launcher3.LauncherStateManager; import com.android.launcher3.LauncherStateManager.StateHandler; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.TouchController; import com.android.quickstep.OverviewInteractionState; import com.android.quickstep.RecentsModel; @@ -52,6 +53,7 @@ import com.android.systemui.shared.system.WindowManagerWrapper; import java.io.ByteArrayOutputStream; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.zip.Deflater; public class UiFactory { @@ -59,24 +61,25 @@ public class UiFactory { public static TouchController[] createTouchControllers(Launcher launcher) { boolean swipeUpEnabled = OverviewInteractionState.INSTANCE.get(launcher) .isSwipeUpGestureEnabled(); - if (!swipeUpEnabled) { - return new TouchController[] { - launcher.getDragController(), - new OverviewToAllAppsTouchController(launcher), - new LauncherTaskViewController(launcher)}; + ArrayList list = new ArrayList<>(); + list.add(launcher.getDragController()); + + if (!swipeUpEnabled || launcher.getDeviceProfile().isVerticalBarLayout()) { + list.add(new OverviewToAllAppsTouchController(launcher)); } + if (launcher.getDeviceProfile().isVerticalBarLayout()) { - return new TouchController[] { - launcher.getDragController(), - new OverviewToAllAppsTouchController(launcher), - new LandscapeEdgeSwipeController(launcher), - new LauncherTaskViewController(launcher)}; + list.add(new LandscapeEdgeSwipeController(launcher)); } else { - return new TouchController[] { - launcher.getDragController(), - new PortraitStatesTouchController(launcher), - new LauncherTaskViewController(launcher)}; + list.add(new PortraitStatesTouchController(launcher)); } + if (FeatureFlags.PULL_DOWN_STATUS_BAR && Utilities.IS_DEBUG_DEVICE + && !launcher.getDeviceProfile().isMultiWindowMode + && !launcher.getDeviceProfile().isVerticalBarLayout()) { + list.add(new StatusBarTouchController(launcher)); + } + list.add(new LauncherTaskViewController(launcher)); + return list.toArray(new TouchController[list.size()]); } public static void setOnTouchControllersChangedListener(Context context, Runnable listener) { diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java index 05fc33f35e..b67d35bd6c 100644 --- a/src/com/android/launcher3/config/BaseFlags.java +++ b/src/com/android/launcher3/config/BaseFlags.java @@ -36,6 +36,10 @@ abstract class BaseFlags { // Feature flag to enable moving the QSB on the 0th screen of the workspace. public static final boolean QSB_ON_FIRST_SCREEN = true; + + //Feature flag to enable pulling down navigation shade from workspace. + public static final boolean PULL_DOWN_STATUS_BAR = true; + // When enabled the all-apps icon is not added to the hotseat. public static final boolean NO_ALL_APPS_ICON = true; diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java index fd9157e218..7b09bcce71 100644 --- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java +++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2018 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. diff --git a/src/com/android/launcher3/touch/TouchEventTranslator.java b/src/com/android/launcher3/touch/TouchEventTranslator.java new file mode 100644 index 0000000000..8a5c93264c --- /dev/null +++ b/src/com/android/launcher3/touch/TouchEventTranslator.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2018 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.launcher3.touch; + +import android.graphics.PointF; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; +import android.util.SparseLongArray; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; + +import com.android.launcher3.Utilities.Consumer; + +/** + * To minimize the size of the MotionEvent, historic events are not copied and passed via the + * listener. + */ +public class TouchEventTranslator { + + private static final String TAG = "TouchEventTranslator"; + private static final boolean DEBUG = false; + + private class DownState { + long timeStamp; + float downY; + public DownState(long timeStamp, float downY) { + this.timeStamp = timeStamp; + this.downY = downY; + } + }; + private final DownState ZERO = new DownState(0, 0f); + + private final Consumer mListener; + + private final SparseArray mDownEvents; + private final SparseArray mFingers; + + private final SparseArray> mCache; + + public TouchEventTranslator(Consumer listener) { + mDownEvents = new SparseArray<>(); + mFingers = new SparseArray<>(); + mCache = new SparseArray<>(); + + mListener = listener; + } + + public void reset() { + mDownEvents.clear(); + mFingers.clear(); + } + + public float getDownY() { + return mDownEvents.get(0).downY; + } + + public void setDownParameters(int idx, MotionEvent e) { + DownState ev = new DownState(e.getEventTime(), e.getY(idx)); + mDownEvents.append(idx, ev); + } + + public void dispatchDownEvents(MotionEvent ev) { + for(int i = 0; i < ev.getPointerCount() && i < mDownEvents.size(); i++) { + int pid = ev.getPointerId(i); + put(pid, ev.getX(i), 0, mDownEvents.get(i).timeStamp, ev); + } + } + + public void processMotionEvent(MotionEvent ev) { + if (DEBUG) { + printSamples(TAG + " processMotionEvent", ev); + } + int index = ev.getActionIndex(); + float x = ev.getX(index); + float y = ev.getY(index) - mDownEvents.get(index, ZERO).downY; + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_POINTER_DOWN: + int pid = ev.getPointerId(index); + if(mFingers.get(pid, null) != null) { + for(int i=0; i < ev.getPointerCount(); i++) { + pid = ev.getPointerId(i); + position(pid, x, y); + } + generateEvent(ev.getAction(), ev); + } else { + put(pid, x, y, ev); + } + break; + case MotionEvent.ACTION_MOVE: + for(int i=0; i < ev.getPointerCount(); i++) { + pid = ev.getPointerId(i); + position(pid, x, y); + } + generateEvent(ev.getAction(), ev); + break; + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_UP: + pid = ev.getPointerId(index); + lift(pid, index, x, y, ev); + break; + case MotionEvent.ACTION_CANCEL: + cancel(ev); + break; + default: + Log.v(TAG, "Didn't process "); + printSamples(TAG, ev); + + } + } + + private TouchEventTranslator put(int id, float x, float y, MotionEvent ev) { + return put(id, x, y, ev.getEventTime(), ev); + } + + private TouchEventTranslator put(int id, float x, float y, long ms, MotionEvent ev) { + checkFingerExistence(id, false); + boolean isInitialDown = (mFingers.size() == 0); + + mFingers.put(id, new PointF(x, y)); + int n = mFingers.size(); + + if (mCache.get(n) == null) { + PointerProperties[] properties = new PointerProperties[n]; + PointerCoords[] coords = new PointerCoords[n]; + for (int i = 0; i < n; i++) { + properties[i] = new PointerProperties(); + coords[i] = new PointerCoords(); + } + mCache.put(n, new Pair(properties, coords)); + } + + int action; + if (isInitialDown) { + action = MotionEvent.ACTION_DOWN; + } else { + action = MotionEvent.ACTION_POINTER_DOWN; + // Set the id of the changed pointer. + action |= id << MotionEvent.ACTION_POINTER_INDEX_SHIFT; + } + generateEvent(action, ms, ev); + return this; + } + + public TouchEventTranslator position(int id, float x, float y) { + checkFingerExistence(id, true); + mFingers.get(id).set(x, y); + return this; + } + + private TouchEventTranslator lift(int id, int index, MotionEvent ev) { + checkFingerExistence(id, true); + boolean isFinalUp = (mFingers.size() == 1); + int action; + if (isFinalUp) { + action = MotionEvent.ACTION_UP; + } else { + action = MotionEvent.ACTION_POINTER_UP; + // Set the id of the changed pointer. + action |= index << MotionEvent.ACTION_POINTER_INDEX_SHIFT; + } + generateEvent(action, ev); + mFingers.remove(id); + return this; + } + + private TouchEventTranslator lift(int id, int index, float x, float y, MotionEvent ev) { + checkFingerExistence(id, true); + mFingers.get(id).set(x, y); + return lift(id, index, ev); + } + + public TouchEventTranslator cancel(MotionEvent ev) { + generateEvent(MotionEvent.ACTION_CANCEL, ev); + mFingers.clear(); + return this; + } + + private void checkFingerExistence(int id, boolean shouldExist) { + if (shouldExist != (mFingers.get(id, null) != null)) { + throw new IllegalArgumentException( + shouldExist ? "Finger does not exist" : "Finger already exists"); + } + } + + + /** + * Used to debug MotionEvents being sent/received. + */ + public void printSamples(String msg, MotionEvent ev) { + System.out.printf("%s %s", msg, MotionEvent.actionToString(ev.getActionMasked())); + final int pointerCount = ev.getPointerCount(); + System.out.printf("#%d/%d", ev.getPointerId(ev.getActionIndex()), pointerCount); + System.out.printf(" t=%d:", ev.getEventTime()); + for (int p = 0; p < pointerCount; p++) { + System.out.printf(" id=%d: (%f,%f)", + ev.getPointerId(p), ev.getX(p), ev.getY(p)); + } + System.out.println(); + } + + private void generateEvent(int action, MotionEvent ev) { + generateEvent(action, ev.getEventTime(), ev); + } + + private void generateEvent(int action, long ms, MotionEvent ev) { + Pair state = getFingerState(); + MotionEvent event = MotionEvent.obtain( + mDownEvents.get(0).timeStamp, + ms, + action, + state.first.length, + state.first, + state.second, + ev.getMetaState(), + ev.getButtonState() /* buttonState */, + ev.getXPrecision() /* xPrecision */, + ev.getYPrecision() /* yPrecision */, + ev.getDeviceId(), + ev.getEdgeFlags(), + ev.getSource(), + ev.getFlags() /* flags */); + if (DEBUG) { + printSamples(TAG + " generateEvent", event); + } + mListener.accept(event); + event.recycle(); + } + + /** + * Returns the description of the fingers' state expected by MotionEvent. + */ + private Pair getFingerState() { + int nFingers = mFingers.size(); + + Pair result = mCache.get(nFingers); + PointerProperties[] properties = result.first; + PointerCoords[] coordinates = result.second; + + int index = 0; + for (int i = 0; i < mFingers.size(); i++) { + int id = mFingers.keyAt(i); + PointF location = mFingers.get(id); + + PointerProperties property = properties[i]; + property.id = id; + property.toolType = MotionEvent.TOOL_TYPE_FINGER; + properties[index] = property; + + PointerCoords coordinate = coordinates[i]; + coordinate.x = location.x; + coordinate.y = location.y; + coordinate.pressure = 1.0f; + coordinates[index] = coordinate; + + index++; + } + return mCache.get(nFingers); + } +}