/* * Copyright (C) 2014 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.recents.views; import android.app.ActivityManager; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.ViewDebug; import com.android.internal.policy.DividerSnapAlgorithm; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.events.EventBus; import com.android.systemui.recents.events.activity.ConfigurationChangedEvent; import com.android.systemui.recents.events.ui.HideIncompatibleAppOverlayEvent; import com.android.systemui.recents.events.ui.ShowIncompatibleAppOverlayEvent; import com.android.systemui.recents.events.ui.dragndrop.DragDropTargetChangedEvent; import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent; import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent; import com.android.systemui.recents.events.ui.dragndrop.DragStartInitializeDropTargetsEvent; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskStack; import java.util.ArrayList; /** * Represents the dock regions for each orientation. */ class DockRegion { public static TaskStack.DockState[] PHONE_LANDSCAPE = { // We only allow docking to the left in landscape for now on small devices TaskStack.DockState.LEFT }; public static TaskStack.DockState[] PHONE_PORTRAIT = { // We only allow docking to the top for now on small devices TaskStack.DockState.TOP }; public static TaskStack.DockState[] TABLET_LANDSCAPE = { TaskStack.DockState.LEFT, TaskStack.DockState.RIGHT }; public static TaskStack.DockState[] TABLET_PORTRAIT = PHONE_PORTRAIT; } /** * Handles touch events for a RecentsView. */ public class RecentsViewTouchHandler { private RecentsView mRv; @ViewDebug.ExportedProperty(deepExport=true, prefix="drag_task") private Task mDragTask; @ViewDebug.ExportedProperty(deepExport=true, prefix="drag_task_view_") private TaskView mTaskView; @ViewDebug.ExportedProperty(category="recents") private Point mTaskViewOffset = new Point(); @ViewDebug.ExportedProperty(category="recents") private Point mDownPos = new Point(); @ViewDebug.ExportedProperty(category="recents") private boolean mDragRequested; @ViewDebug.ExportedProperty(category="recents") private boolean mIsDragging; private float mDragSlop; private DropTarget mLastDropTarget; private DividerSnapAlgorithm mDividerSnapAlgorithm; private ArrayList<DropTarget> mDropTargets = new ArrayList<>(); private ArrayList<TaskStack.DockState> mVisibleDockStates = new ArrayList<>(); public RecentsViewTouchHandler(RecentsView rv) { mRv = rv; mDragSlop = ViewConfiguration.get(rv.getContext()).getScaledTouchSlop(); updateSnapAlgorithm(); } private void updateSnapAlgorithm() { Rect insets = new Rect(); SystemServicesProxy.getInstance(mRv.getContext()).getStableInsets(insets); mDividerSnapAlgorithm = DividerSnapAlgorithm.create(mRv.getContext(), insets); } /** * Registers a new drop target for the current drag only. */ public void registerDropTargetForCurrentDrag(DropTarget target) { mDropTargets.add(target); } /** * Returns the preferred dock states for the current orientation. */ public TaskStack.DockState[] getDockStatesForCurrentOrientation() { boolean isLandscape = mRv.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; RecentsConfiguration config = Recents.getConfiguration(); if (config.isLargeScreen) { return isLandscape ? DockRegion.TABLET_LANDSCAPE : DockRegion.TABLET_PORTRAIT; } else { return isLandscape ? DockRegion.PHONE_LANDSCAPE : DockRegion.PHONE_PORTRAIT; } } /** * Returns the set of visible dock states for this current drag. */ public ArrayList<TaskStack.DockState> getVisibleDockStates() { return mVisibleDockStates; } /** Touch preprocessing for handling below */ public boolean onInterceptTouchEvent(MotionEvent ev) { handleTouchEvent(ev); return mDragRequested; } /** Handles touch events once we have intercepted them */ public boolean onTouchEvent(MotionEvent ev) { handleTouchEvent(ev); return mDragRequested; } /**** Events ****/ public final void onBusEvent(DragStartEvent event) { SystemServicesProxy ssp = Recents.getSystemServices(); mRv.getParent().requestDisallowInterceptTouchEvent(true); mDragRequested = true; // We defer starting the actual drag handling until the user moves past the drag slop mIsDragging = false; mDragTask = event.task; mTaskView = event.taskView; mDropTargets.clear(); int[] recentsViewLocation = new int[2]; mRv.getLocationInWindow(recentsViewLocation); mTaskViewOffset.set(mTaskView.getLeft() - recentsViewLocation[0] + event.tlOffset.x, mTaskView.getTop() - recentsViewLocation[1] + event.tlOffset.y); float x = mDownPos.x - mTaskViewOffset.x; float y = mDownPos.y - mTaskViewOffset.y; mTaskView.setTranslationX(x); mTaskView.setTranslationY(y); mVisibleDockStates.clear(); if (ActivityManager.supportsMultiWindow() && !ssp.hasDockedTask() && mDividerSnapAlgorithm.isSplitScreenFeasible()) { Recents.logDockAttempt(mRv.getContext(), event.task.getTopComponent(), event.task.resizeMode); if (!event.task.isDockable) { EventBus.getDefault().send(new ShowIncompatibleAppOverlayEvent()); } else { // Add the dock state drop targets (these take priority) TaskStack.DockState[] dockStates = getDockStatesForCurrentOrientation(); for (TaskStack.DockState dockState : dockStates) { registerDropTargetForCurrentDrag(dockState); dockState.update(mRv.getContext()); mVisibleDockStates.add(dockState); } } } // Request other drop targets to register themselves EventBus.getDefault().send(new DragStartInitializeDropTargetsEvent(event.task, event.taskView, this)); } public final void onBusEvent(DragEndEvent event) { if (!mDragTask.isDockable) { EventBus.getDefault().send(new HideIncompatibleAppOverlayEvent()); } mDragRequested = false; mDragTask = null; mTaskView = null; mLastDropTarget = null; } public final void onBusEvent(ConfigurationChangedEvent event) { if (event.fromDisplayDensityChange || event.fromDeviceOrientationChange) { updateSnapAlgorithm(); } } /** * Handles dragging touch events */ private void handleTouchEvent(MotionEvent ev) { int action = ev.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: mDownPos.set((int) ev.getX(), (int) ev.getY()); break; case MotionEvent.ACTION_MOVE: { float evX = ev.getX(); float evY = ev.getY(); float x = evX - mTaskViewOffset.x; float y = evY - mTaskViewOffset.y; if (mDragRequested) { if (!mIsDragging) { mIsDragging = Math.hypot(evX - mDownPos.x, evY - mDownPos.y) > mDragSlop; } if (mIsDragging) { int width = mRv.getMeasuredWidth(); int height = mRv.getMeasuredHeight(); DropTarget currentDropTarget = null; // Give priority to the current drop target to retain the touch handling if (mLastDropTarget != null) { if (mLastDropTarget.acceptsDrop((int) evX, (int) evY, width, height, mRv.mSystemInsets, true /* isCurrentTarget */)) { currentDropTarget = mLastDropTarget; } } // Otherwise, find the next target to handle this event if (currentDropTarget == null) { for (DropTarget target : mDropTargets) { if (target.acceptsDrop((int) evX, (int) evY, width, height, mRv.mSystemInsets, false /* isCurrentTarget */)) { currentDropTarget = target; break; } } } if (mLastDropTarget != currentDropTarget) { mLastDropTarget = currentDropTarget; EventBus.getDefault().send(new DragDropTargetChangedEvent(mDragTask, currentDropTarget)); } } mTaskView.setTranslationX(x); mTaskView.setTranslationY(y); } break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { if (mDragRequested) { boolean cancelled = action == MotionEvent.ACTION_CANCEL; if (cancelled) { EventBus.getDefault().send(new DragDropTargetChangedEvent(mDragTask, null)); } EventBus.getDefault().send(new DragEndEvent(mDragTask, mTaskView, !cancelled ? mLastDropTarget : null)); break; } } } } }