Drag/drop APIs and infrastructure
A View initiates a drag-and-drop operation (hereafter just called a "drag")
by calling its startDrag(ClipData) method. Within the processing of that
call, two callbacks are made into the originating View. The first is to
onMeasureDragThumbnail(). Similarly to the core onMeasure() method, this
callback must respond by calling setDragThumbnailDimension(width, height) to
declare the size of the drag thumbnail image that should be used. Following
this, the View's onDrawDragThumbnail(canvas) method will be invoked to
actually produce the bits of the thumbnail image.
If all goes well, startDrag() will return 'true', and the drag is off and
running. (The other arguments to startDrag() provide reconciliation between
the current finger position and where the thumbnail should be placed on
the screen relative to it.)
Potential receipients of the ClipData behind the drag are notified by a
new dispatch mechanism, roughly parallel to motion event dispatch. The core
routine is the View's onDragEvent(event) callback, with the mechanics of
dispatch itself being routed through dispatchDragEvent(event) -- as in
the case of motion events, the dispatch logic is in ViewGroup, with leaf
View objects not needing to consider the dispatch flow.
Several different event 'actions' are delivered through this dispatch
mechanism:
ACTION_DRAG_STARTED: this event is propagated to every View in every window
(including windows created during the course of a drag). It serves as a
global notification that a drag has started with a payload whose matching
ClipDescription is supplied with the event. A View that is prepared to
consume the data described in this event should return 'true' from their
onDragEvent() method, and ideally will also make some visible on-screen
indication that they are a potential target of the drop.
ACTION_DRAG_ENTERED: this event is sent once when the drag point
enters the View's bounds. It is an opportunity for the View to set up
feedback that they are the one who will see the drop if the finger goes
up now.
ACTION_DRAG_LOCATION: when the drag point is over a given View, that
View will receive a stream of DRAG_LOCATION events, providing an
opportunity for the View to show visual feedback tied to the drag point.
ACTION_DRAG_EXITED: like DRAG_ENTERED, but called when the drag point
leaves the View's bounds. The View should undo any visuals meant to
emphasize their being the hovered-over target.
ACTION_DROP: when the drag ends at a given point, the View under that
point is sent this event, with the full ClipData of the payload.
ACTION_DRAG_ENDED: paralleling the DRAG_STARTED action, this is the global
broadcast that the drag has ended and all Views should return to their
normal visual state. This happens after the DROP event.
Change-Id: Ia8d0fb1516bce8c735d87ffd101af0976d7e84b6
diff --git a/api/current.xml b/api/current.xml
index 403820d..709705b 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -189612,6 +189612,217 @@
>
</field>
</class>
+<class name="DragEvent"
+ extends="java.lang.Object"
+ abstract="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<implements name="android.os.Parcelable">
+</implements>
+<method name="describeContents"
+ return="int"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getAction"
+ return="int"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getClipData"
+ return="android.content.ClipData"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getClipDescription"
+ return="android.content.ClipDescription"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getX"
+ return="float"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="getY"
+ return="float"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="obtain"
+ return="android.view.DragEvent"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="obtain"
+ return="android.view.DragEvent"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="true"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="action" type="int">
+</parameter>
+<parameter name="x" type="float">
+</parameter>
+<parameter name="y" type="float">
+</parameter>
+<parameter name="description" type="android.content.ClipDescription">
+</parameter>
+<parameter name="data" type="android.content.ClipData">
+</parameter>
+</method>
+<method name="recycle"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</method>
+<method name="writeToParcel"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="dest" type="android.os.Parcel">
+</parameter>
+<parameter name="flags" type="int">
+</parameter>
+</method>
+<field name="ACTION_DRAG_ENDED"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="4"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="ACTION_DRAG_ENTERED"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="5"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="ACTION_DRAG_EXITED"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="6"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="ACTION_DRAG_LOCATION"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="2"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="ACTION_DRAG_STARTED"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="1"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="ACTION_DROP"
+ type="int"
+ transient="false"
+ volatile="false"
+ value="3"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+<field name="CREATOR"
+ type="android.os.Parcelable.Creator"
+ transient="false"
+ volatile="false"
+ static="true"
+ final="true"
+ deprecated="not deprecated"
+ visibility="public"
+>
+</field>
+</class>
<class name="FocusFinder"
extends="java.lang.Object"
abstract="false"
@@ -198568,6 +198779,19 @@
<parameter name="hint" type="int">
</parameter>
</method>
+<method name="dispatchDragEvent"
+ return="boolean"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="event" type="android.view.DragEvent">
+</parameter>
+</method>
<method name="dispatchDraw"
return="void"
abstract="false"
@@ -200533,6 +200757,19 @@
<parameter name="hint" type="int">
</parameter>
</method>
+<method name="onDragEvent"
+ return="boolean"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="protected"
+>
+<parameter name="event" type="android.view.DragEvent">
+</parameter>
+</method>
<method name="onDraw"
return="void"
abstract="false"
@@ -200546,6 +200783,19 @@
<parameter name="canvas" type="android.graphics.Canvas">
</parameter>
</method>
+<method name="onDrawDragThumbnail"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="protected"
+>
+<parameter name="canvas" type="android.graphics.Canvas">
+</parameter>
+</method>
<method name="onDrawScrollBars"
return="void"
abstract="false"
@@ -200739,6 +200989,17 @@
<parameter name="heightMeasureSpec" type="int">
</parameter>
</method>
+<method name="onMeasureDragThumbnail"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="protected"
+>
+</method>
<method name="onRestoreInstanceState"
return="void"
abstract="false"
@@ -201404,6 +201665,21 @@
<parameter name="contentDescription" type="java.lang.CharSequence">
</parameter>
</method>
+<method name="setDragThumbnailDimension"
+ return="void"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="true"
+ deprecated="not deprecated"
+ visibility="protected"
+>
+<parameter name="width" type="int">
+</parameter>
+<parameter name="height" type="int">
+</parameter>
+</method>
<method name="setDrawingCacheBackgroundColor"
return="void"
abstract="false"
diff --git a/core/java/android/content/res/CompatibilityInfo.java b/core/java/android/content/res/CompatibilityInfo.java
index d0ba590..406b091 100644
--- a/core/java/android/content/res/CompatibilityInfo.java
+++ b/core/java/android/content/res/CompatibilityInfo.java
@@ -18,6 +18,7 @@
import android.content.pm.ApplicationInfo;
import android.graphics.Canvas;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.DisplayMetrics;
@@ -363,6 +364,17 @@
}
/**
+ * Translate a Point in screen coordinates into the app window's coordinates.
+ */
+ public void translatePointInScreenToAppWindow(PointF point) {
+ final float scale = applicationInvertedScale;
+ if (scale != 1.0f) {
+ point.x *= scale;
+ point.y *= scale;
+ }
+ }
+
+ /**
* Translate the location of the sub window.
* @param params
*/
diff --git a/core/java/android/view/DragEvent.aidl b/core/java/android/view/DragEvent.aidl
new file mode 100644
index 0000000..f08943f
--- /dev/null
+++ b/core/java/android/view/DragEvent.aidl
@@ -0,0 +1,19 @@
+/*
+** Copyright 2010, 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 android.view;
+
+parcelable DragEvent;
diff --git a/core/java/android/view/DragEvent.java b/core/java/android/view/DragEvent.java
new file mode 100644
index 0000000..47c6d3c
--- /dev/null
+++ b/core/java/android/view/DragEvent.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2010 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 android.view;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/** !!! TODO: real docs */
+public class DragEvent implements Parcelable {
+ private static final boolean TRACK_RECYCLED_LOCATION = false;
+
+ int mAction;
+ float mX, mY;
+ ClipDescription mClipDescription;
+ ClipData mClipData;
+
+ private DragEvent mNext;
+ private RuntimeException mRecycledLocation;
+ private boolean mRecycled;
+
+ private static final int MAX_RECYCLED = 10;
+ private static final Object gRecyclerLock = new Object();
+ private static int gRecyclerUsed = 0;
+ private static DragEvent gRecyclerTop = null;
+
+ /**
+ * action constants for DragEvent dispatch
+ */
+ public static final int ACTION_DRAG_STARTED = 1;
+ public static final int ACTION_DRAG_LOCATION = 2;
+ public static final int ACTION_DROP = 3;
+ public static final int ACTION_DRAG_ENDED = 4;
+ public static final int ACTION_DRAG_ENTERED = 5;
+ public static final int ACTION_DRAG_EXITED = 6;
+
+ /* hide the constructor behind package scope */
+ DragEvent() {
+ }
+
+ public static DragEvent obtain() {
+ return DragEvent.obtain(0, 0f, 0f, null, null);
+ }
+
+ public static DragEvent obtain(int action, float x, float y,
+ ClipDescription description, ClipData data) {
+ final DragEvent ev;
+ synchronized (gRecyclerLock) {
+ if (gRecyclerTop == null) {
+ return new DragEvent();
+ }
+ ev = gRecyclerTop;
+ gRecyclerTop = ev.mNext;
+ gRecyclerUsed -= 1;
+ }
+ ev.mRecycledLocation = null;
+ ev.mRecycled = false;
+ ev.mNext = null;
+
+ ev.mAction = action;
+ ev.mX = x;
+ ev.mY = y;
+ ev.mClipDescription = description;
+ ev.mClipData = data;
+
+ return ev;
+ }
+
+ public int getAction() {
+ return mAction;
+ }
+
+ public float getX() {
+ return mX;
+ }
+
+ public float getY() {
+ return mY;
+ }
+
+ public ClipData getClipData() {
+ return mClipData;
+ }
+
+ public ClipDescription getClipDescription() {
+ return mClipDescription;
+ }
+
+ /**
+ * Recycle the DragEvent, to be re-used by a later caller. After calling
+ * this function you must never touch the event again.
+ */
+ public final void recycle() {
+ // Ensure recycle is only called once!
+ if (TRACK_RECYCLED_LOCATION) {
+ if (mRecycledLocation != null) {
+ throw new RuntimeException(toString() + " recycled twice!", mRecycledLocation);
+ }
+ mRecycledLocation = new RuntimeException("Last recycled here");
+ } else {
+ if (mRecycled) {
+ throw new RuntimeException(toString() + " recycled twice!");
+ }
+ mRecycled = true;
+ }
+
+ mClipData = null;
+ mClipDescription = null;
+
+ synchronized (gRecyclerLock) {
+ if (gRecyclerUsed < MAX_RECYCLED) {
+ gRecyclerUsed++;
+ mNext = gRecyclerTop;
+ gRecyclerTop = this;
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "DragEvent{" + Integer.toHexString(System.identityHashCode(this))
+ + " action=" + mAction + " @ (" + mX + ", " + mY + ") desc=" + mClipDescription
+ + " data=" + mClipData
+ + "}";
+ }
+
+ /* Parcelable interface */
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mAction);
+ dest.writeFloat(mX);
+ dest.writeFloat(mY);
+ if (mClipData == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mClipData.writeToParcel(dest, flags);
+ }
+ if (mClipDescription == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mClipDescription.writeToParcel(dest, flags);
+ }
+ }
+
+ public static final Parcelable.Creator<DragEvent> CREATOR =
+ new Parcelable.Creator<DragEvent>() {
+ public DragEvent createFromParcel(Parcel in) {
+ DragEvent event = DragEvent.obtain();
+ event.mAction = in.readInt();
+ event.mX = in.readFloat();
+ event.mY = in.readFloat();
+ if (in.readInt() != 0) {
+ event.mClipData = ClipData.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() != 0) {
+ event.mClipDescription = ClipDescription.CREATOR.createFromParcel(in);
+ }
+ return event;
+ }
+
+ public DragEvent[] newArray(int size) {
+ return new DragEvent[size];
+ }
+ };
+}
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index 921018a..6b9cda0 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -17,10 +17,13 @@
package android.view;
+import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
+import android.view.DragEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -64,4 +67,9 @@
void dispatchWallpaperCommand(String action, int x, int y,
int z, in Bundle extras, boolean sync);
+
+ /**
+ * Drag/drop events
+ */
+ void dispatchDragEvent(in DragEvent event);
}
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 7f10b76..79ea5b6 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -17,6 +17,7 @@
package android.view;
+import android.content.ClipData;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.graphics.Region;
@@ -116,6 +117,30 @@
boolean performHapticFeedback(IWindow window, int effectId, boolean always);
/**
+ * Allocate the drag's thumbnail surface. Also assigns a token that identifies
+ * the drag to the OS and passes that as the return value. A return value of
+ * null indicates failure.
+ */
+ IBinder prepareDrag(IWindow window, boolean localOnly,
+ int thumbnailWidth, int thumbnailHeight, out Surface outSurface);
+
+ /**
+ * Initiate the drag operation itself
+ */
+ boolean performDrag(IWindow window, IBinder dragToken, float touchX, float touchY,
+ float thumbCenterX, float thumbCenterY, in ClipData data);
+
+ /**
+ * Tell the OS that we've just dragged into a View that is willing to accept the drop
+ */
+ void dragRecipientEntered(IWindow window);
+
+ /**
+ * Tell the OS that we've just dragged *off* of a View that was willing to accept the drop
+ */
+ void dragRecipientExited(IWindow window);
+
+ /**
* For windows with the wallpaper behind them, and the wallpaper is
* larger than the screen, set the offset within the screen.
* For multi screen launcher type applications, xstep and ystep indicate
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index f2d134b..595bac8 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -16,6 +16,8 @@
package android.view;
+import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -53,6 +55,7 @@
import android.util.Pools;
import android.util.SparseArray;
import android.view.ContextMenu.ContextMenuInfo;
+import android.view.DragEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityEventSource;
import android.view.accessibility.AccessibilityManager;
@@ -614,6 +617,7 @@
*/
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
private static final boolean DBG = false;
+ static final boolean DEBUG_DRAG = true;
/**
* The logging tag used by this class with android.util.Log.
@@ -2030,6 +2034,15 @@
private int mTouchSlop;
/**
+ * Cache drag/drop state
+ *
+ */
+ boolean mCanAcceptDrop;
+ private boolean mIsCurrentDropTarget;
+ private int mThumbnailWidth;
+ private int mThumbnailHeight;
+
+ /**
* Simple constructor to use when creating a view from code.
*
* @param context The Context the view is running in, through which it can
@@ -9762,6 +9775,137 @@
}
/**
+ * Drag and drop. App calls startDrag(), then callbacks to onMeasureDragThumbnail()
+ * and onDrawDragThumbnail() happen, then the drag operation is handed over to the
+ * OS.
+ * !!! TODO: real docs
+ * @hide
+ */
+ public final boolean startDrag(ClipData data, float touchX, float touchY,
+ float thumbnailTouchX, float thumbnailTouchY, boolean myWindowOnly) {
+ if (DEBUG_DRAG) {
+ Log.d(VIEW_LOG_TAG, "startDrag: touch=(" + touchX + "," + touchY
+ + ") thumb=(" + thumbnailTouchX + "," + thumbnailTouchY
+ + ") data=" + data + " local=" + myWindowOnly);
+ }
+ boolean okay = false;
+
+ measureThumbnail(); // throws if the view fails to specify dimensions
+
+ Surface surface = new Surface();
+ try {
+ IBinder token = mAttachInfo.mSession.prepareDrag(mAttachInfo.mWindow,
+ myWindowOnly, mThumbnailWidth, mThumbnailHeight, surface);
+ if (DEBUG_DRAG) Log.d(VIEW_LOG_TAG, "prepareDrag returned token=" + token
+ + " surface=" + surface);
+ if (token != null) {
+ Canvas canvas = surface.lockCanvas(null);
+ onDrawDragThumbnail(canvas);
+ surface.unlockCanvasAndPost(canvas);
+
+ okay = mAttachInfo.mSession.performDrag(mAttachInfo.mWindow, token,
+ touchX, touchY, thumbnailTouchX, thumbnailTouchX, data);
+ if (DEBUG_DRAG) Log.d(VIEW_LOG_TAG, "performDrag returned " + okay);
+ }
+ } catch (Exception e) {
+ Log.e(VIEW_LOG_TAG, "Unable to initiate drag", e);
+ surface.destroy();
+ }
+
+ return okay;
+ }
+
+ private void measureThumbnail() {
+ mPrivateFlags &= ~MEASURED_DIMENSION_SET;
+
+ onMeasureDragThumbnail();
+
+ // flag not set, setDragThumbnailDimension() was not invoked, we raise
+ // an exception to warn the developer
+ if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
+ throw new IllegalStateException("onMeasureDragThumbnail() did not set the"
+ + " measured dimension by calling setDragThumbnailDimension()");
+ }
+
+ if (DEBUG_DRAG) {
+ Log.d(VIEW_LOG_TAG, "Drag thumb measured: w=" + mThumbnailWidth
+ + " h=" + mThumbnailHeight);
+ }
+ }
+
+ /**
+ * The View must call this method from onMeasureDragThumbnail() in order to
+ * specify the dimensions of the drag thumbnail image.
+ *
+ * @param width
+ * @param height
+ */
+ protected final void setDragThumbnailDimension(int width, int height) {
+ mPrivateFlags |= MEASURED_DIMENSION_SET;
+ mThumbnailWidth = width;
+ mThumbnailHeight = height;
+ }
+
+ /**
+ * The default implementation specifies a drag thumbnail that matches the
+ * View's current size and appearance.
+ */
+ protected void onMeasureDragThumbnail() {
+ setDragThumbnailDimension(getWidth(), getHeight());
+ }
+
+ /**
+ * The default implementation just draws the current View appearance as the thumbnail
+ * @param canvas
+ */
+ protected void onDrawDragThumbnail(Canvas canvas) {
+ draw(canvas);
+ }
+
+ /**
+ * Drag-and-drop event dispatch. The event.getAction() verb is one of the DragEvent
+ * constants DRAG_STARTED_EVENT, DRAG_EVENT, DROP_EVENT, and DRAG_ENDED_EVENT.
+ *
+ * For DRAG_STARTED_EVENT, event.getClipDescription() describes the content
+ * being dragged. onDragEvent() should return 'true' if the view can handle
+ * a drop of that content. A view that returns 'false' here will receive no
+ * further calls to onDragEvent() about the drag/drop operation.
+ *
+ * For DRAG_ENTERED, event.getClipDescription() describes the content being
+ * dragged. This will be the same content description passed in the
+ * DRAG_STARTED_EVENT invocation.
+ *
+ * For DRAG_EXITED, event.getClipDescription() describes the content being
+ * dragged. This will be the same content description passed in the
+ * DRAG_STARTED_EVENT invocation. The view should return to its approriate
+ * drag-acceptance visual state.
+ *
+ * For DRAG_LOCATION_EVENT, event.getX() and event.getY() give the location in View
+ * coordinates of the current drag point. The view must return 'true' if it
+ * can accept a drop of the current drag content, false otherwise.
+ *
+ * For DROP_EVENT, event.getX() and event.getY() give the location of the drop
+ * within the view; also, event.getClipData() returns the full data payload
+ * being dropped. The view should return 'true' if it consumed the dropped
+ * content, 'false' if it did not.
+ *
+ * For DRAG_ENDED_EVENT, the 'event' argument may be null. The view should return
+ * to its normal visual state.
+ */
+ protected boolean onDragEvent(DragEvent event) {
+ return false;
+ }
+
+ /**
+ * Views typically don't need to override dispatchDragEvent(); it just calls
+ * onDragEvent(what, event) and passes the result up appropriately.
+ *
+ */
+ public boolean dispatchDragEvent(DragEvent event) {
+ return onDragEvent(event);
+ }
+
+ /**
* This needs to be a better API (NOT ON VIEW) before it is exposed. If
* it is ever exposed at all.
* @hide
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 1e86f74..8a3db38 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -19,6 +19,8 @@
import android.animation.LayoutTransition;
import com.android.internal.R;
+import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
@@ -26,6 +28,7 @@
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
@@ -33,6 +36,7 @@
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.Slog;
import android.util.SparseArray;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Animation;
@@ -68,6 +72,7 @@
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
private static final boolean DBG = false;
+ private static final String TAG = "ViewGroup";
/**
* Views which have been hidden or removed which need to be animated on
@@ -105,6 +110,15 @@
*/
private Transformation mInvalidationTransformation;
+ // View currently under an ongoing drag
+ private View mCurrentDragView;
+
+ // Does this group have a child that can accept the current drag payload?
+ private boolean mChildAcceptsDrag;
+
+ // Used during drag dispatch
+ private final PointF mLocalPoint = new PointF();
+
// Layout animation
private LayoutAnimationController mLayoutAnimationController;
private Animation.AnimationListener mAnimationListener;
@@ -815,6 +829,136 @@
/**
* {@inheritDoc}
+ *
+ * !!! TODO: write real docs
+ */
+ @Override
+ public boolean dispatchDragEvent(DragEvent event) {
+ boolean retval = false;
+ final float tx = event.mX;
+ final float ty = event.mY;
+
+ // !!! BUGCHECK: If we have a ViewGroup, we must necessarily have a ViewRoot,
+ // so we don't need to check getRootView() for null here?
+ ViewRoot root = (ViewRoot)(getRootView().getParent());
+
+ // Dispatch down the view hierarchy
+ switch (event.mAction) {
+ case DragEvent.ACTION_DRAG_STARTED: {
+ // clear state to recalculate which views we drag over
+ root.setDragFocus(event, null);
+
+ // Now dispatch down to our children, caching the responses
+ mChildAcceptsDrag = false;
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ final boolean handled = children[i].dispatchDragEvent(event);
+ children[i].mCanAcceptDrop = handled;
+ if (handled) {
+ mChildAcceptsDrag = true;
+ }
+ }
+
+ // Return HANDLED if one of our children can accept the drag
+ if (mChildAcceptsDrag) {
+ retval = true;
+ }
+ } break;
+
+ case DragEvent.ACTION_DRAG_ENDED: {
+ // Notify all of our children that the drag is over
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = 0; i < count; i++) {
+ children[i].dispatchDragEvent(event);
+ }
+ // We consider drag-ended to have been handled if one of our children
+ // had offered to handle the drag.
+ if (mChildAcceptsDrag) {
+ retval = true;
+ }
+ } break;
+
+ case DragEvent.ACTION_DRAG_LOCATION: {
+ // Find the [possibly new] drag target
+ final View target = findFrontmostDroppableChildAt(event.mX, event.mY, mLocalPoint);
+
+ // If we've changed apparent drag target, tell the view root which view
+ // we're over now. This will in turn send out DRAG_ENTERED / DRAG_EXITED
+ // notifications as appropriate.
+ if (mCurrentDragView != target) {
+ root.setDragFocus(event, target);
+ mCurrentDragView = target;
+ }
+
+ // Dispatch the actual drag location notice, localized into its coordinates
+ if (target != null) {
+ event.mX = mLocalPoint.x;
+ event.mY = mLocalPoint.y;
+
+ retval = target.dispatchDragEvent(event);
+
+ event.mX = tx;
+ event.mY = ty;
+ }
+ } break;
+
+ case DragEvent.ACTION_DROP: {
+ if (View.DEBUG_DRAG) Slog.d(TAG, "Drop event: " + event);
+ View target = findFrontmostDroppableChildAt(event.mX, event.mY, mLocalPoint);
+ if (target != null) {
+ event.mX = mLocalPoint.x;
+ event.mY = mLocalPoint.y;
+ retval = target.dispatchDragEvent(event);
+ event.mX = tx;
+ event.mY = ty;
+ }
+ } break;
+ }
+
+ // If none of our children could handle the event, try here
+ if (!retval) {
+ retval = onDragEvent(event);
+ }
+ return retval;
+ }
+
+ // Find the frontmost child view that lies under the given point, and calculate
+ // the position within its own local coordinate system.
+ View findFrontmostDroppableChildAt(float x, float y, PointF outLocalPoint) {
+ final float scrolledX = x + mScrollX;
+ final float scrolledY = y + mScrollY;
+ final int count = mChildrenCount;
+ final View[] children = mChildren;
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = children[i];
+ if (child.mCanAcceptDrop == false) {
+ continue;
+ }
+
+ float localX = scrolledX - child.mLeft;
+ float localY = scrolledY - child.mTop;
+ if (!child.hasIdentityMatrix() && mAttachInfo != null) {
+ // non-identity matrix: transform the point into the view's coordinates
+ final float[] localXY = mAttachInfo.mTmpTransformLocation;
+ localXY[0] = localX;
+ localXY[1] = localY;
+ child.getInverseMatrix().mapPoints(localXY);
+ localX = localXY[0];
+ localY = localXY[1];
+ }
+ if (localX >= 0 && localY >= 0 && localX < (child.mRight - child.mLeft) &&
+ localY < (child.mBottom - child.mTop)) {
+ outLocalPoint.set(localX, localY);
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
*/
@Override
public boolean dispatchKeyEventPreIme(KeyEvent event) {
diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java
index 77ba6fe..c63f7d6 100644
--- a/core/java/android/view/ViewRoot.java
+++ b/core/java/android/view/ViewRoot.java
@@ -24,6 +24,7 @@
import android.graphics.Canvas;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.*;
@@ -45,6 +46,8 @@
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.ComponentCallbacks;
import android.content.Context;
import android.app.ActivityManagerNative;
@@ -198,6 +201,11 @@
final ViewConfiguration mViewConfiguration;
+ /* Drag/drop */
+ ClipDescription mDragDescription;
+ View mCurrentDragView;
+ final PointF mDragPoint = new PointF();
+
/**
* see {@link #playSoundEffect(int)}
*/
@@ -1670,6 +1678,7 @@
public final static int FINISH_INPUT_CONNECTION = 1012;
public final static int CHECK_FOCUS = 1013;
public final static int CLOSE_SYSTEM_DIALOGS = 1014;
+ public final static int DISPATCH_DRAG_EVENT = 1015;
@Override
public void handleMessage(Message msg) {
@@ -1845,6 +1854,9 @@
mView.onCloseSystemDialogs((String)msg.obj);
}
} break;
+ case DISPATCH_DRAG_EVENT: {
+ handleDragEvent((DragEvent)msg.obj);
+ } break;
}
}
@@ -2434,6 +2446,87 @@
}
}
+ /* drag/drop */
+ private void handleDragEvent(DragEvent event) {
+ // From the root, only drag start/end/location are dispatched. entered/exited
+ // are determined and dispatched by the viewgroup hierarchy, who then report
+ // that back here for ultimate reporting back to the framework.
+ if (mView != null && mAdded) {
+ final int what = event.mAction;
+
+ if (what == DragEvent.ACTION_DRAG_EXITED) {
+ // A direct EXITED event means that the window manager knows we've just crossed
+ // a window boundary, so the current drag target within this one must have
+ // just been exited. Send it the usual notifications and then we're done
+ // for now.
+ setDragFocus(event, null);
+ } else {
+ // Cache the drag description when the operation starts, then fill it in
+ // on subsequent calls as a convenience
+ if (what == DragEvent.ACTION_DRAG_STARTED) {
+ mDragDescription = event.mClipDescription;
+ } else {
+ event.mClipDescription = mDragDescription;
+ }
+
+ // For events with a [screen] location, translate into window coordinates
+ if ((what == DragEvent.ACTION_DRAG_LOCATION) || (what == DragEvent.ACTION_DROP)) {
+ mDragPoint.set(event.mX, event.mY);
+ if (mTranslator != null) {
+ mTranslator.translatePointInScreenToAppWindow(mDragPoint);
+ }
+
+ if (mCurScrollY != 0) {
+ mDragPoint.offset(0, mCurScrollY);
+ }
+
+ event.mX = mDragPoint.x;
+ event.mY = mDragPoint.y;
+ }
+
+ // Remember who the current drag target is pre-dispatch
+ final View prevDragView = mCurrentDragView;
+
+ // Now dispatch the drag/drop event
+ mView.dispatchDragEvent(event);
+
+ // If we changed apparent drag target, tell the OS about it
+ if (prevDragView != mCurrentDragView) {
+ try {
+ if (prevDragView != null) {
+ sWindowSession.dragRecipientExited(mWindow);
+ }
+ if (mCurrentDragView != null) {
+ sWindowSession.dragRecipientEntered(mWindow);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to note drag target change");
+ }
+ mCurrentDragView = prevDragView;
+ }
+ }
+ }
+ event.recycle();
+ }
+
+ public void setDragFocus(DragEvent event, View newDragTarget) {
+ final int action = event.mAction;
+ // If we've dragged off of a view, send it the EXITED message
+ if (mCurrentDragView != newDragTarget) {
+ if (mCurrentDragView != null) {
+ event.mAction = DragEvent.ACTION_DRAG_EXITED;
+ mCurrentDragView.dispatchDragEvent(event);
+ }
+ }
+ // If we've dragged over a new view, send it the ENTERED message
+ if (newDragTarget != null) {
+ event.mAction = DragEvent.ACTION_DRAG_ENTERED;
+ newDragTarget.dispatchDragEvent(event);
+ }
+ mCurrentDragView = newDragTarget;
+ event.mAction = action; // restore the event's original state
+ }
+
private AudioManager getAudioManager() {
if (mView == null) {
throw new IllegalStateException("getAudioManager called when there is no mView");
@@ -2725,7 +2818,12 @@
msg.obj = reason;
sendMessage(msg);
}
-
+
+ public void dispatchDragEvent(DragEvent event) {
+ Message msg = obtainMessage(DISPATCH_DRAG_EVENT, event);
+ sendMessage(msg);
+ }
+
/**
* The window is getting focus so if there is anything focused/selected
* send an {@link AccessibilityEvent} to announce that.
@@ -2936,6 +3034,14 @@
}
}
}
+
+ /* Drag/drop */
+ public void dispatchDragEvent(DragEvent event) {
+ final ViewRoot viewRoot = mViewRoot.get();
+ if (viewRoot != null) {
+ viewRoot.dispatchDragEvent(event);
+ }
+ }
}
/**
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index dc3b44d..eddd04e 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -347,6 +347,13 @@
public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14;
/**
+ * Window type: the drag-and-drop pseudowindow. There is only one
+ * drag layer (at most), and it is placed on top of all other windows.
+ * @hide
+ */
+ public static final int TYPE_DRAG = FIRST_SYSTEM_WINDOW+15;
+
+ /**
* End of types of system windows.
*/
public static final int LAST_SYSTEM_WINDOW = 2999;
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index 4da74e6..d5213db 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -16,11 +16,14 @@
package com.android.internal.view;
+import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
+import android.view.DragEvent;
import android.view.IWindow;
import android.view.IWindowSession;
import android.view.KeyEvent;
@@ -66,7 +69,10 @@
}
}
}
-
+
+ public void dispatchDragEvent(DragEvent event) {
+ }
+
public void dispatchWallpaperCommand(String action, int x, int y,
int z, Bundle extras, boolean sync) {
if (sync) {
diff --git a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
index c047522..bdf24d6 100755
--- a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
+++ b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
@@ -89,6 +89,7 @@
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+import static android.view.WindowManager.LayoutParams.TYPE_DRAG;
import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD;
import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
import static android.view.WindowManager.LayoutParams.TYPE_PHONE;
@@ -152,8 +153,11 @@
// responsible for power management when displayed.
static final int KEYGUARD_LAYER = 14;
static final int KEYGUARD_DIALOG_LAYER = 15;
+ // the drag layer: input for drag-and-drop is associated with this window,
+ // which sits above all other focusable windows
+ static final int DRAG_LAYER = 16;
// things in here CAN NOT take focus, but are shown on top of everything else.
- static final int SYSTEM_OVERLAY_LAYER = 16;
+ static final int SYSTEM_OVERLAY_LAYER = 17;
static final int APPLICATION_MEDIA_SUBLAYER = -2;
static final int APPLICATION_MEDIA_OVERLAY_SUBLAYER = -1;
@@ -839,6 +843,8 @@
return TOAST_LAYER;
case TYPE_WALLPAPER:
return WALLPAPER_LAYER;
+ case TYPE_DRAG:
+ return DRAG_LAYER;
}
Log.e(TAG, "Unknown window type: " + type);
return APPLICATION_LAYER;
diff --git a/services/java/com/android/server/InputManager.java b/services/java/com/android/server/InputManager.java
index fe306b3..a960097 100644
--- a/services/java/com/android/server/InputManager.java
+++ b/services/java/com/android/server/InputManager.java
@@ -342,6 +342,7 @@
if (toChannel == null) {
throw new IllegalArgumentException("toChannel must not be null.");
}
+ Slog.d(TAG, "transferring touch focus");
return nativeTransferTouchFocus(fromChannel, toChannel);
}
diff --git a/services/java/com/android/server/WindowManagerService.java b/services/java/com/android/server/WindowManagerService.java
index 7100cc5..0c1417d 100644
--- a/services/java/com/android/server/WindowManagerService.java
+++ b/services/java/com/android/server/WindowManagerService.java
@@ -40,6 +40,7 @@
import com.android.internal.app.IBatteryStats;
import com.android.internal.policy.PolicyManager;
import com.android.internal.policy.impl.PhoneWindowManager;
+import com.android.internal.view.BaseInputHandler;
import com.android.internal.view.IInputContext;
import com.android.internal.view.IInputMethodClient;
import com.android.internal.view.IInputMethodManager;
@@ -51,6 +52,8 @@
import android.app.IActivityManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
+import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -93,6 +96,7 @@
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.view.Display;
+import android.view.DragEvent;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.IApplicationToken;
@@ -104,6 +108,8 @@
import android.view.InputChannel;
import android.view.InputDevice;
import android.view.InputEvent;
+import android.view.InputHandler;
+import android.view.InputQueue;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
@@ -159,6 +165,7 @@
static final boolean DEBUG_STARTING_WINDOW = false;
static final boolean DEBUG_REORDER = false;
static final boolean DEBUG_WALLPAPER = false;
+ static final boolean DEBUG_DRAG = true;
static final boolean SHOW_TRANSACTIONS = false;
static final boolean HIDE_STACK_CRAWLS = true;
@@ -486,6 +493,311 @@
boolean mTurnOnScreen;
/**
+ * Drag/drop state
+ */
+ class DragState {
+ IBinder mToken;
+ Surface mSurface;
+ boolean mLocalOnly;
+ ClipData mData;
+ ClipDescription mDataDescription;
+ float mThumbOffsetX, mThumbOffsetY;
+ InputChannel mServerChannel, mClientChannel;
+ WindowState mTargetWindow;
+ ArrayList<WindowState> mNotifiedWindows;
+ boolean mDragEnded;
+
+ private final Rect tmpRect = new Rect();
+
+ DragState(IBinder token, Surface surface, boolean localOnly) {
+ mToken = token;
+ mSurface = surface;
+ mLocalOnly = localOnly;
+ mNotifiedWindows = new ArrayList<WindowState>();
+ }
+
+ void reset() {
+ if (mSurface != null) {
+ mSurface.destroy();
+ }
+ mSurface = null;
+ mLocalOnly = false;
+ mToken = null;
+ mData = null;
+ mThumbOffsetX = mThumbOffsetY = 0;
+ mNotifiedWindows = null;
+ }
+
+ void register() {
+ if (DEBUG_DRAG) Slog.d(TAG, "registering drag input channel");
+ if (mClientChannel != null) {
+ Slog.e(TAG, "Duplicate register of drag input channel");
+ } else {
+ InputChannel[] channels = InputChannel.openInputChannelPair("drag");
+ mServerChannel = channels[0];
+ mClientChannel = channels[1];
+ mInputManager.registerInputChannel(mServerChannel);
+ InputQueue.registerInputChannel(mClientChannel, mDragInputHandler,
+ mH.getLooper().getQueue());
+ }
+ }
+
+ void unregister() {
+ if (DEBUG_DRAG) Slog.d(TAG, "unregistering drag input channel");
+ if (mClientChannel == null) {
+ Slog.e(TAG, "Unregister of nonexistent drag input channel");
+ } else {
+ mInputManager.unregisterInputChannel(mServerChannel);
+ InputQueue.unregisterInputChannel(mClientChannel);
+ mClientChannel.dispose();
+ mClientChannel = null;
+ mServerChannel = null;
+ }
+ }
+
+ /* call out to each visible window/session informing it about the drag
+ */
+ void broadcastDragStartedLw() {
+ // Cache a base-class instance of the clip metadata so that parceling
+ // works correctly in calling out to the apps.
+ mDataDescription = new ClipDescription(mData);
+ mNotifiedWindows.clear();
+
+ if (DEBUG_DRAG) {
+ Slog.d(TAG, "broadcasting DRAG_STARTED of " + mDataDescription);
+ }
+
+ DragEvent evt = DragEvent.obtain(DragEvent.ACTION_DRAG_STARTED, 0, 0,
+ mDataDescription, null);
+ for (WindowState ws : mWindows) {
+ sendDragStartedLw(ws, evt);
+ }
+ evt.recycle();
+ }
+
+ /* helper - send a caller-provided event, presumed to be DRAG_STARTED, if the
+ * designated window is potentially a drop recipient. There are race situations
+ * around DRAG_ENDED broadcast, so we make sure that once we've declared that
+ * the drag has ended, we never send out another DRAG_STARTED for this drag action.
+ */
+ private void sendDragStartedLw(WindowState newWin, final DragEvent event) {
+ if (!mDragEnded && newWin.isPotentialDragTarget()) {
+ try {
+ newWin.mClient.dispatchDragEvent(event);
+ // track each window that we've notified that the drag is starting
+ mNotifiedWindows.add(newWin);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Unable to drag-start window " + newWin);
+ }
+ }
+ }
+
+ /* helper - construct and send a DRAG_STARTED event only if the window has not
+ * previously been notified, i.e. it became visible after the drag operation
+ * was begun. This is a rare case.
+ */
+ private void sendDragStartedIfNeededLw(WindowState newWin) {
+ // If we have sent the drag-started, we needn't do so again
+ for (WindowState ws : mNotifiedWindows) {
+ if (ws == newWin) {
+ return;
+ }
+ }
+ if (DEBUG_DRAG) {
+ Slog.d(TAG, "sending DRAG_STARTED to new window " + newWin);
+ }
+ DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_STARTED, 0, 0,
+ mDataDescription, null);
+ sendDragStartedLw(newWin, event);
+ event.recycle();
+ }
+
+ void broadcastDragEnded() {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG, "broadcasting DRAG_ENDED");
+ }
+ DragEvent evt = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, 0, 0, null, null);
+ synchronized (mWindowMap) {
+ for (WindowState ws: mNotifiedWindows) {
+ try {
+ ws.mClient.dispatchDragEvent(evt);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Unable to drag-end window " + ws);
+ }
+ }
+ mNotifiedWindows.clear();
+ mDragEnded = true;
+ }
+ evt.recycle();
+ }
+
+ void notifyMoveLw(float x, float y) {
+ WindowState touchedWin = getTouchedWinAtPointLw(x, y);
+ try {
+ // have we dragged over a new window?
+ if ((touchedWin != mTargetWindow) && (mTargetWindow != null)) {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG, "sending DRAG_EXITED to " + mTargetWindow);
+ }
+ // force DRAG_EXITED_EVENT if appropriate
+ DragEvent evt = DragEvent.obtain(DragEvent.ACTION_DRAG_EXITED,
+ 0, 0, null, null);
+ mTargetWindow.mClient.dispatchDragEvent(evt);
+ evt.recycle();
+ }
+ if (touchedWin != null) {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG, "sending DRAG_LOCATION to " + touchedWin);
+ }
+ DragEvent evt = DragEvent.obtain(DragEvent.ACTION_DRAG_LOCATION,
+ x, y, null, null);
+ touchedWin.mClient.dispatchDragEvent(evt);
+ evt.recycle();
+ }
+ } catch (RemoteException e) {
+ Slog.w(TAG, "can't send drag notification to windows");
+ }
+ mTargetWindow = touchedWin;
+ }
+
+ // Tell the drop target about the data, and then broadcast the drag-ended notification
+ void notifyDropLw(float x, float y) {
+ WindowState touchedWin = getTouchedWinAtPointLw(x, y);
+ if (touchedWin != null) {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG, "sending DROP to " + touchedWin);
+ }
+ DragEvent evt = DragEvent.obtain(DragEvent.ACTION_DROP, x, y, null, mData);
+ try {
+ touchedWin.mClient.dispatchDragEvent(evt);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "can't send drop notification to win " + touchedWin);
+ }
+ evt.recycle();
+ }
+ }
+
+ // Find the visible, touch-deliverable window under the given point
+ private WindowState getTouchedWinAtPointLw(float xf, float yf) {
+ WindowState touchedWin = null;
+ final int x = (int) xf;
+ final int y = (int) yf;
+ final ArrayList<WindowState> windows = mWindows;
+ final int N = windows.size();
+ for (int i = N - 1; i >= 0; i--) {
+ WindowState child = windows.get(i);
+ final int flags = child.mAttrs.flags;
+ if (!child.isVisibleLw()) {
+ // not visible == don't tell about drags
+ continue;
+ }
+ if ((flags & WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) != 0) {
+ // not touchable == don't tell about drags
+ continue;
+ }
+ // account for the window's decor etc
+ tmpRect.set(child.mFrame);
+ if (child.mTouchableInsets == ViewTreeObserver
+ .InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT) {
+ // The point is inside of the window if it is
+ // inside the frame, AND the content part of that
+ // frame that was given by the application.
+ tmpRect.left += child.mGivenContentInsets.left;
+ tmpRect.top += child.mGivenContentInsets.top;
+ tmpRect.right -= child.mGivenContentInsets.right;
+ tmpRect.bottom -= child.mGivenContentInsets.bottom;
+ } else if (child.mTouchableInsets == ViewTreeObserver
+ .InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE) {
+ // The point is inside of the window if it is
+ // inside the frame, AND the visible part of that
+ // frame that was given by the application.
+ tmpRect.left += child.mGivenVisibleInsets.left;
+ tmpRect.top += child.mGivenVisibleInsets.top;
+ tmpRect.right -= child.mGivenVisibleInsets.right;
+ tmpRect.bottom -= child.mGivenVisibleInsets.bottom;
+ }
+ final int touchFlags = flags &
+ (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
+ if (tmpRect.contains(x, y) || touchFlags == 0) {
+ // Found it
+ touchedWin = child;
+ break;
+ }
+ }
+
+ return touchedWin;
+ }
+ }
+
+ DragState mDragState = null;
+ private final InputHandler mDragInputHandler = new BaseInputHandler() {
+ @Override
+ public void handleMotion(MotionEvent event, Runnable finishedCallback) {
+ boolean endDrag = false;
+ final float newX = event.getRawX();
+ final float newY = event.getRawY();
+
+ try {
+ if (mDragState != null) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ if (DEBUG_DRAG) {
+ Slog.w(TAG, "Unexpected ACTION_DOWN in drag layer");
+ }
+ } break;
+
+ case MotionEvent.ACTION_MOVE: {
+ synchronized (mWindowMap) {
+ // move the surface to the latest touch point
+ mDragState.mSurface.openTransaction();
+ mDragState.mSurface.setPosition((int)(newX - mDragState.mThumbOffsetX),
+ (int)(newY - mDragState.mThumbOffsetY));
+ mDragState.mSurface.closeTransaction();
+
+ // tell the involved window(s) where we are
+ mDragState.notifyMoveLw(newX, newY);
+ }
+ } break;
+
+ case MotionEvent.ACTION_UP: {
+ if (DEBUG_DRAG) Slog.d(TAG, "Got UP on move channel; dropping at "
+ + newX + "," + newY);
+ synchronized (mWindowMap) {
+ mDragState.notifyDropLw(newX, newY);
+ }
+ endDrag = true;
+ } break;
+
+ case MotionEvent.ACTION_CANCEL: {
+ if (DEBUG_DRAG) Slog.d(TAG, "Drag cancelled!");
+ endDrag = true;
+ } break;
+ }
+
+ if (endDrag) {
+ if (DEBUG_DRAG) Slog.d(TAG, "Drag ended; tearing down state");
+ // tell all the windows that the drag has ended
+ mDragState.broadcastDragEnded();
+
+ // stop intercepting input
+ mDragState.unregister();
+ mInputMonitor.updateInputWindowsLw();
+
+ // free our resources and drop all the object references
+ mDragState.reset();
+ mDragState = null;
+ }
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Exception caught by drag handleMotion", e);
+ } finally {
+ finishedCallback.run();
+ }
+ }
+ };
+
+ /**
* Whether the UI is currently running in touch mode (not showing
* navigational focus because the user is directly pressing the screen).
*/
@@ -5046,7 +5358,61 @@
mPolicy.adjustConfigurationLw(config);
return true;
}
-
+
+ // -------------------------------------------------------------
+ // Drag and drop
+ // -------------------------------------------------------------
+
+ IBinder prepareDragSurface(IWindow window, SurfaceSession session,
+ boolean localOnly, int width, int height, Surface outSurface) {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG, "prepare drag surface: w=" + width + " h=" + height
+ + " local=" + localOnly + " win=" + window
+ + " asbinder=" + window.asBinder());
+ }
+
+ final int callerPid = Binder.getCallingPid();
+ final long origId = Binder.clearCallingIdentity();
+ IBinder token = null;
+
+ try {
+ synchronized (mWindowMap) {
+ try {
+ // !!! TODO: fail if the given window does not currently have touch focus?
+
+ if (mDragState == null) {
+ Surface surface = new Surface(session, callerPid, "drag surface", 0,
+ width, height, PixelFormat.TRANSLUCENT, Surface.HIDDEN);
+ outSurface.copyFrom(surface);
+ token = new Binder();
+ mDragState = new DragState(token, surface, localOnly);
+ mDragState.mSurface = surface;
+ mDragState.mLocalOnly = localOnly;
+ token = mDragState.mToken = new Binder();
+
+ // 5 second timeout for this window to actually begin the drag
+ mH.removeMessages(H.DRAG_START_TIMEOUT, window);
+ Message msg = mH.obtainMessage(H.DRAG_START_TIMEOUT, window.asBinder());
+ mH.sendMessageDelayed(msg, 5000);
+ } else {
+ Slog.w(TAG, "Drag already in progress");
+ }
+ } catch (Surface.OutOfResourcesException e) {
+ Slog.e(TAG, "Can't allocate drag surface w=" + width + " h=" + height, e);
+ if (mDragState != null) {
+ mDragState.reset();
+ mDragState = null;
+ }
+
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+
+ return token;
+ }
+
// -------------------------------------------------------------
// Input Events and Focus Management
// -------------------------------------------------------------
@@ -5145,7 +5511,42 @@
return null;
}
-
+
+ private void addDragInputWindow(InputWindowList windowList) {
+ final InputWindow inputWindow = windowList.add();
+ inputWindow.inputChannel = mDragState.mServerChannel;
+ inputWindow.name = "drag";
+ inputWindow.layoutParamsFlags = 0;
+ inputWindow.layoutParamsType = WindowManager.LayoutParams.TYPE_DRAG;
+ inputWindow.dispatchingTimeoutNanos = DEFAULT_INPUT_DISPATCHING_TIMEOUT_NANOS;
+ inputWindow.visible = true;
+ inputWindow.canReceiveKeys = false;
+ inputWindow.hasFocus = true;
+ inputWindow.hasWallpaper = false;
+ inputWindow.paused = false;
+ inputWindow.layer = mPolicy.windowTypeToLayerLw(inputWindow.layoutParamsType)
+ * TYPE_LAYER_MULTIPLIER
+ + TYPE_LAYER_OFFSET;
+ inputWindow.ownerPid = Process.myPid();
+ inputWindow.ownerUid = Process.myUid();
+
+ // The drag window covers the entire display
+ inputWindow.frameLeft = 0;
+ inputWindow.frameTop = 0;
+ inputWindow.frameRight = mDisplay.getWidth();
+ inputWindow.frameBottom = mDisplay.getHeight();
+
+ inputWindow.visibleFrameLeft = inputWindow.frameLeft;
+ inputWindow.visibleFrameTop = inputWindow.frameTop;
+ inputWindow.visibleFrameRight = inputWindow.frameRight;
+ inputWindow.visibleFrameBottom = inputWindow.frameBottom;
+
+ inputWindow.touchableAreaLeft = inputWindow.frameLeft;
+ inputWindow.touchableAreaTop = inputWindow.frameTop;
+ inputWindow.touchableAreaRight = inputWindow.frameRight;
+ inputWindow.touchableAreaBottom = inputWindow.frameBottom;
+ }
+
/* Updates the cached window information provided to the input dispatcher. */
public void updateInputWindowsLw() {
// Populate the input window list with information about all of the windows that
@@ -5154,6 +5555,16 @@
// out to be difficult because only the native code knows for sure which window
// currently has touch focus.
final ArrayList<WindowState> windows = mWindows;
+
+ // If there's a drag in flight, provide a pseudowindow to catch drag input
+ final boolean inDrag = (mDragState != null);
+ if (inDrag) {
+ if (DEBUG_DRAG) {
+ Log.d(TAG, "Inserting drag window");
+ }
+ addDragInputWindow(mTempInputWindows);
+ }
+
final int N = windows.size();
for (int i = N - 1; i >= 0; i--) {
final WindowState child = windows.get(i);
@@ -5169,7 +5580,13 @@
final boolean isVisible = child.isVisibleLw();
final boolean hasWallpaper = (child == mWallpaperTarget)
&& (type != WindowManager.LayoutParams.TYPE_KEYGUARD);
-
+
+ // If there's a drag in progress and 'child' is a potential drop target,
+ // make sure it's been told about the drag
+ if (inDrag && isVisible) {
+ mDragState.sendDragStartedIfNeededLw(child);
+ }
+
// Add a window to our list of input windows.
final InputWindow inputWindow = mTempInputWindows.add();
inputWindow.inputChannel = child.mInputChannel;
@@ -5741,6 +6158,87 @@
}
}
+ /* Drag/drop */
+ public IBinder prepareDrag(IWindow window, boolean localOnly,
+ int width, int height, Surface outSurface) {
+ return prepareDragSurface(window, mSurfaceSession, localOnly,
+ width, height, outSurface);
+ }
+
+ public boolean performDrag(IWindow window, IBinder dragToken,
+ float touchX, float touchY, float thumbCenterX, float thumbCenterY,
+ ClipData data) {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG, "perform drag: win=" + window + " data=" + data);
+ }
+
+ synchronized (mWindowMap) {
+ if (mDragState == null) {
+ Slog.w(TAG, "No drag prepared");
+ throw new IllegalStateException("performDrag() without prepareDrag()");
+ }
+
+ if (dragToken != mDragState.mToken) {
+ Slog.w(TAG, "Performing mismatched drag");
+ throw new IllegalStateException("performDrag() does not match prepareDrag()");
+ }
+
+ WindowState callingWin = windowForClientLocked(null, window, false);
+ if (callingWin == null) {
+ Slog.w(TAG, "Bad requesting window " + window);
+ return false; // !!! TODO: throw here?
+ }
+
+ // !!! TODO: if input is not still focused on the initiating window, fail
+ // the drag initiation (e.g. an alarm window popped up just as the application
+ // called performDrag()
+
+ mH.removeMessages(H.DRAG_START_TIMEOUT, window.asBinder());
+
+ // !!! TODO: call into the input monitor to sever the current touch event flow
+ // and redirect to the drag "window"; also extract the current touch (x, y)
+ // in screen coordinates
+
+ mDragState.register();
+ mInputMonitor.updateInputWindowsLw();
+ mInputManager.transferTouchFocus(callingWin.mInputChannel,
+ mDragState.mServerChannel);
+
+ mDragState.mData = data;
+ mDragState.broadcastDragStartedLw();
+
+ // remember the thumb offsets for later
+ mDragState.mThumbOffsetX = thumbCenterX;
+ mDragState.mThumbOffsetY = thumbCenterY;
+
+ // Make the surface visible at the proper location
+ final Surface surface = mDragState.mSurface;
+ surface.openTransaction();
+ try {
+ surface.setPosition((int)(touchX - thumbCenterX),
+ (int)(touchY - thumbCenterY));
+ surface.setAlpha(.5f);
+ surface.show();
+ } finally {
+ surface.closeTransaction();
+ }
+ }
+
+ return true; // success!
+ }
+
+ public void dragRecipientEntered(IWindow window) {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG, "Drag into new candidate view @ " + window);
+ }
+ }
+
+ public void dragRecipientExited(IWindow window) {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG, "Drag from old candidate view @ " + window);
+ }
+ }
+
public void setWallpaperPosition(IBinder window, float x, float y, float xStep, float yStep) {
synchronized(mWindowMap) {
long ident = Binder.clearCallingIdentity();
@@ -6882,6 +7380,15 @@
}
/**
+ * Can this window possibly be a drag/drop target? The test here is
+ * a combination of the above "visible now" with the check that the
+ * Input Manager uses when discarding windows from input consideration.
+ */
+ boolean isPotentialDragTarget() {
+ return isVisibleNow() && (mInputChannel != null) && !mRemoved;
+ }
+
+ /**
* Same as isVisible(), but we also count it as visible between the
* call to IWindowSession.add() and the first relayout().
*/
@@ -7810,6 +8317,7 @@
public static final int APP_FREEZE_TIMEOUT = 17;
public static final int SEND_NEW_CONFIGURATION = 18;
public static final int REPORT_WINDOWS_CHANGE = 19;
+ public static final int DRAG_START_TIMEOUT = 20;
private Session mLastReportedHold;
@@ -8152,6 +8660,20 @@
break;
}
+ case DRAG_START_TIMEOUT: {
+ IBinder win = (IBinder)msg.obj;
+ if (DEBUG_DRAG) {
+ Slog.w(TAG, "Timeout starting drag by win " + win);
+ }
+ synchronized (mWindowMap) {
+ // !!! TODO: ANR the app that has failed to start the drag in time
+ if (mDragState != null) {
+ mDragState.reset();
+ mDragState = null;
+ }
+ }
+ }
+
}
}
}
diff --git a/services/jni/com_android_server_InputManager.cpp b/services/jni/com_android_server_InputManager.cpp
index e1e54fc..bf6840c 100644
--- a/services/jni/com_android_server_InputManager.cpp
+++ b/services/jni/com_android_server_InputManager.cpp
@@ -1289,6 +1289,7 @@
static jboolean android_server_InputManager_nativeTransferTouchFocus(JNIEnv* env,
jclass clazz, jobject fromChannelObj, jobject toChannelObj) {
if (checkInputManagerUnitialized(env)) {
+ LOGD("input manager uninitialized; bailing");
return false;
}
@@ -1298,6 +1299,7 @@
android_view_InputChannel_getInputChannel(env, toChannelObj);
if (fromChannel == NULL || toChannel == NULL) {
+ LOGD("bailing because from=%p to=%p", fromChannel, toChannel);
return false;
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java
index f91f601..0553f5e 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java
@@ -30,6 +30,8 @@
import com.android.tools.layoutlib.create.MethodAdapter;
import com.android.tools.layoutlib.create.OverrideMethod;
+import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -46,6 +48,7 @@
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.BridgeInflater;
+import android.view.DragEvent;
import android.view.InputChannel;
import android.view.IWindow;
import android.view.IWindowSession;
@@ -1073,6 +1076,33 @@
}
@SuppressWarnings("unused")
+ public IBinder prepareDrag(IWindow window, boolean localOnly,
+ int thumbnailWidth, int thumbnailHeight, Surface outSurface)
+ throws RemoteException {
+ // pass for now
+ return null;
+ }
+
+ @SuppressWarnings("unused")
+ public boolean performDrag(IWindow window, IBinder dragToken,
+ float touchX, float touchY, float thumbCenterX, float thumbCenterY,
+ ClipData data)
+ throws RemoteException {
+ // pass for now
+ return false;
+ }
+
+ @SuppressWarnings("unused")
+ public void dragRecipientEntered(IWindow window) throws RemoteException {
+ // pass for now
+ }
+
+ @SuppressWarnings("unused")
+ public void dragRecipientExited(IWindow window) throws RemoteException {
+ // pass for now
+ }
+
+ @SuppressWarnings("unused")
public void setWallpaperPosition(IBinder window, float x, float y,
float xStep, float yStep) {
// pass for now.
@@ -1170,6 +1200,11 @@
// pass for now.
}
+ @SuppressWarnings("unused")
+ public void dispatchDragEvent(DragEvent event) {
+ // pass for now.
+ }
+
public IBinder asBinder() {
// pass for now.
return null;