Merge "Add readEvent method to MtpDevice."
diff --git a/api/current.txt b/api/current.txt
index f963c17..2d133aa 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -18133,6 +18133,7 @@
     method public boolean importFile(int, java.lang.String);
     method public boolean importFile(int, android.os.ParcelFileDescriptor);
     method public boolean open(android.hardware.usb.UsbDeviceConnection);
+    method public android.mtp.MtpEvent readEvent(android.os.CancellationSignal);
     method public boolean sendObject(int, int, android.os.ParcelFileDescriptor);
     method public android.mtp.MtpObjectInfo sendObjectInfo(android.mtp.MtpObjectInfo);
   }
@@ -18144,6 +18145,11 @@
     method public final java.lang.String getVersion();
   }
 
+  public class MtpEvent {
+    ctor public MtpEvent();
+    method public int getEventCode();
+  }
+
   public final class MtpObjectInfo {
     method public final int getAssociationDesc();
     method public final int getAssociationType();
diff --git a/api/system-current.txt b/api/system-current.txt
index 1084f30..93551da 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -19645,6 +19645,7 @@
     method public boolean importFile(int, java.lang.String);
     method public boolean importFile(int, android.os.ParcelFileDescriptor);
     method public boolean open(android.hardware.usb.UsbDeviceConnection);
+    method public android.mtp.MtpEvent readEvent(android.os.CancellationSignal);
     method public boolean sendObject(int, int, android.os.ParcelFileDescriptor);
     method public android.mtp.MtpObjectInfo sendObjectInfo(android.mtp.MtpObjectInfo);
   }
@@ -19656,6 +19657,11 @@
     method public final java.lang.String getVersion();
   }
 
+  public class MtpEvent {
+    ctor public MtpEvent();
+    method public int getEventCode();
+  }
+
   public final class MtpObjectInfo {
     method public final int getAssociationDesc();
     method public final int getAssociationType();
diff --git a/media/java/android/mtp/MtpDevice.java b/media/java/android/mtp/MtpDevice.java
index 3cd157e..95cb520 100644
--- a/media/java/android/mtp/MtpDevice.java
+++ b/media/java/android/mtp/MtpDevice.java
@@ -18,6 +18,8 @@
 
 import android.hardware.usb.UsbDevice;
 import android.hardware.usb.UsbDeviceConnection;
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
 import android.os.ParcelFileDescriptor;
 
 /**
@@ -47,7 +49,7 @@
 
     /**
      * Opens the MTP device.  Once the device is open it takes ownership of the
-     * {@link android.hardware.usb.UsbDeviceConnection}.  
+     * {@link android.hardware.usb.UsbDeviceConnection}.
      * The connection will be closed when you call {@link #close()}
      * The connection will also be closed if this method fails.
      *
@@ -278,6 +280,38 @@
         return native_send_object_info(info);
     }
 
+    /**
+     * Reads an event from the device. It blocks the current thread until it gets an event.
+     * It throws OperationCanceledException if it is cancelled by signal.
+     *
+     * @param signal signal for cancellation
+     * @return obtained event
+     */
+    public MtpEvent readEvent(CancellationSignal signal) {
+        final int handle = native_submit_event_request();
+
+        if (handle < 0) {
+            throw new IllegalStateException("Other thread is reading an event.");
+        }
+
+        if (signal != null) {
+            signal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
+                @Override
+                public void onCancel() {
+                    native_discard_event_request(handle);
+                }
+            });
+        }
+
+        try {
+            return native_reap_event_request(handle);
+        } finally {
+            if (signal != null) {
+                signal.setOnCancelListener(null);
+            }
+        }
+    }
+
     // used by the JNI code
     private long mNativeContext;
 
@@ -297,4 +331,7 @@
     private native boolean native_import_file(int objectHandle, int fd);
     private native boolean native_send_object(int objectHandle, int size, int fd);
     private native MtpObjectInfo native_send_object_info(MtpObjectInfo info);
+    private native int native_submit_event_request();
+    private native MtpEvent native_reap_event_request(int handle);
+    private native void native_discard_event_request(int handle);
 }
diff --git a/media/java/android/mtp/MtpEvent.java b/media/java/android/mtp/MtpEvent.java
new file mode 100644
index 0000000..0fa7d93
--- /dev/null
+++ b/media/java/android/mtp/MtpEvent.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2015 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.mtp;
+
+/**
+ * This class encapsulates information about a MTP event.
+ */
+public class MtpEvent {
+    private int mEventCode;
+
+    /**
+     * Returns event code of MTP event.
+     *
+     * @return event code
+     */
+    public int getEventCode() { return mEventCode; }
+}
diff --git a/media/jni/android_mtp_MtpDevice.cpp b/media/jni/android_mtp_MtpDevice.cpp
index f11329c..3f4d183 100644
--- a/media/jni/android_mtp_MtpDevice.cpp
+++ b/media/jni/android_mtp_MtpDevice.cpp
@@ -46,10 +46,14 @@
 jclass clazz_deviceInfo;
 jclass clazz_storageInfo;
 jclass clazz_objectInfo;
+jclass clazz_event;
+jclass clazz_io_exception;
+jclass clazz_operation_canceled_exception;
 
 jmethodID constructor_deviceInfo;
 jmethodID constructor_storageInfo;
 jmethodID constructor_objectInfo;
+jmethodID constructor_event;
 
 // MtpDeviceInfo fields
 static jfieldID field_deviceInfo_manufacturer;
@@ -86,6 +90,9 @@
 static jfieldID field_objectInfo_dateModified;
 static jfieldID field_objectInfo_keywords;
 
+// MtpEvent fields
+static jfieldID field_event_eventCode;
+
 MtpDevice* get_device_from_object(JNIEnv* env, jobject javaDevice)
 {
     return (MtpDevice*)env->GetLongField(javaDevice, field_context);
@@ -488,6 +495,42 @@
     return result;
 }
 
+static jint android_mtp_MtpDevice_submit_event_request(JNIEnv *env, jobject thiz)
+{
+    MtpDevice* const device = get_device_from_object(env, thiz);
+    if (!device) {
+        env->ThrowNew(clazz_io_exception, "");
+        return -1;
+    }
+    return device->submitEventRequest();
+}
+
+static jobject android_mtp_MtpDevice_reap_event_request(JNIEnv *env, jobject thiz, jint seq)
+{
+    MtpDevice* const device = get_device_from_object(env, thiz);
+    if (!device) {
+        env->ThrowNew(clazz_io_exception, "");
+        return NULL;
+    }
+    const int eventCode = device->reapEventRequest(seq);
+    if (eventCode <= 0) {
+        env->ThrowNew(clazz_operation_canceled_exception, "");
+        return NULL;
+    }
+    jobject result = env->NewObject(clazz_event, constructor_event);
+    env->SetIntField(result, field_event_eventCode, eventCode);
+    return result;
+}
+
+static void android_mtp_MtpDevice_discard_event_request(JNIEnv *env, jobject thiz, jint seq)
+{
+    MtpDevice* const device = get_device_from_object(env, thiz);
+    if (!device) {
+        return;
+    }
+    device->discardEventRequest(seq);
+}
+
 // ----------------------------------------------------------------------------
 
 static const JNINativeMethod gMethods[] = {
@@ -513,7 +556,11 @@
     {"native_import_file",      "(II)Z",(void *)android_mtp_MtpDevice_import_file_to_fd},
     {"native_send_object",      "(III)Z",(void *)android_mtp_MtpDevice_send_object},
     {"native_send_object_info", "(Landroid/mtp/MtpObjectInfo;)Landroid/mtp/MtpObjectInfo;",
-                                        (void *)android_mtp_MtpDevice_send_object_info}
+                                        (void *)android_mtp_MtpDevice_send_object_info},
+    {"native_submit_event_request",  "()I", (void *)android_mtp_MtpDevice_submit_event_request},
+    {"native_reap_event_request",   "(I)Landroid/mtp/MtpEvent;",
+                                            (void *)android_mtp_MtpDevice_reap_event_request},
+    {"native_discard_event_request", "(I)V", (void *)android_mtp_MtpDevice_discard_event_request},
 };
 
 int register_android_mtp_MtpDevice(JNIEnv *env)
@@ -703,6 +750,23 @@
     }
     clazz_objectInfo = (jclass)env->NewGlobalRef(clazz);
 
+    clazz = env->FindClass("android/mtp/MtpEvent");
+    if (clazz == NULL) {
+        ALOGE("Can't find android/mtp/MtpEvent");
+        return -1;
+    }
+    constructor_event = env->GetMethodID(clazz, "<init>", "()V");
+    if (constructor_event == NULL) {
+        ALOGE("Can't find android/mtp/MtpEvent constructor");
+        return -1;
+    }
+    field_event_eventCode = env->GetFieldID(clazz, "mEventCode", "I");
+    if (field_event_eventCode == NULL) {
+        ALOGE("Can't find MtpObjectInfo.mEventCode");
+        return -1;
+    }
+    clazz_event = (jclass)env->NewGlobalRef(clazz);
+
     clazz = env->FindClass("android/mtp/MtpDevice");
     if (clazz == NULL) {
         ALOGE("Can't find android/mtp/MtpDevice");
@@ -713,6 +777,18 @@
         ALOGE("Can't find MtpDevice.mNativeContext");
         return -1;
     }
+    clazz = env->FindClass("java/io/IOException");
+    if (clazz == NULL) {
+        ALOGE("Can't find java.io.IOException");
+        return -1;
+    }
+    clazz_io_exception = (jclass)env->NewGlobalRef(clazz);
+    clazz = env->FindClass("android/os/OperationCanceledException");
+    if (clazz == NULL) {
+        ALOGE("Can't find android.os.OperationCanceledException");
+        return -1;
+    }
+    clazz_operation_canceled_exception = (jclass)env->NewGlobalRef(clazz);
 
     return AndroidRuntime::registerNativeMethods(env,
                 "android/mtp/MtpDevice", gMethods, NELEM(gMethods));
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
index 7cc7413..af7f691 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
@@ -22,12 +22,14 @@
 import android.hardware.usb.UsbManager;
 import android.mtp.MtpConstants;
 import android.mtp.MtpDevice;
+import android.mtp.MtpEvent;
 import android.mtp.MtpObjectInfo;
+import android.os.CancellationSignal;
 import android.os.ParcelFileDescriptor;
-import android.provider.DocumentsContract.Document;
-import android.provider.DocumentsContract;
 import android.util.SparseArray;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.io.FileNotFoundException;
 import java.io.IOException;
 
@@ -188,7 +190,13 @@
         }
     }
 
-    private MtpDevice getDevice(int deviceId) throws IOException {
+    @VisibleForTesting
+    MtpEvent readEvent(int deviceId, CancellationSignal signal) throws IOException {
+        final MtpDevice device = getDevice(deviceId);
+        return device.readEvent(signal);
+    }
+
+    private synchronized MtpDevice getDevice(int deviceId) throws IOException {
         final MtpDevice device = mDevices.get(deviceId);
         if (device == null) {
             throw new IOException("USB device " + deviceId + " is not opened.");
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpManagerTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpManagerTest.java
index 2c1f115..5547771 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpManagerTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpManagerTest.java
@@ -16,35 +16,101 @@
 
 package com.android.mtp;
 
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.hardware.usb.UsbDevice;
 import android.hardware.usb.UsbDeviceConnection;
 import android.hardware.usb.UsbManager;
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
 import android.test.InstrumentationTestCase;
 
+import java.io.IOException;
 import java.util.HashMap;
+import java.util.concurrent.CountDownLatch;
 
+@RealDeviceTest
 public class MtpManagerTest extends InstrumentationTestCase {
-    @RealDeviceTest
-    public void testBasic() throws Exception {
-        final UsbDevice usbDevice = findDevice();
-        final MtpManager manager = new MtpManager(getContext());
-        manager.openDevice(usbDevice.getDeviceId());
-        waitForStorages(manager, usbDevice.getDeviceId());
-        manager.closeDevice(usbDevice.getDeviceId());
+    private static final String ACTION_USB_PERMISSION =
+            "com.android.mtp.USB_PERMISSION";
+    private static final int TIMEOUT_MS = 1000;
+    UsbManager mUsbManager;
+    MtpManager mManager;
+    UsbDevice mUsbDevice;
+    int mRequest;
+
+    @Override
+    public void setUp() throws Exception {
+        mUsbManager = getContext().getSystemService(UsbManager.class);
+        mUsbDevice = findDevice();
+        mManager = new MtpManager(getContext());
+        mManager.openDevice(mUsbDevice.getDeviceId());
+        waitForStorages(mManager, mUsbDevice.getDeviceId());
+    }
+
+    @Override
+    public void tearDown() throws IOException {
+        mManager.closeDevice(mUsbDevice.getDeviceId());
+    }
+
+    public void testCancelEvent() throws Exception {
+        final CancellationSignal signal = new CancellationSignal();
+        final Thread thread = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    mManager.readEvent(mUsbDevice.getDeviceId(), signal);
+                } catch (OperationCanceledException | IOException e) {
+                    show(e.getMessage());
+                }
+            }
+        };
+        thread.start();
+        Thread.sleep(TIMEOUT_MS);
+        signal.cancel();
+        thread.join(TIMEOUT_MS);
+    }
+
+    private void requestPermission(UsbDevice device) throws InterruptedException {
+        if (mUsbManager.hasPermission(device)) {
+            return;
+        }
+        final CountDownLatch latch = new CountDownLatch(1);
+        final BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                latch.countDown();
+                getInstrumentation().getTargetContext().unregisterReceiver(this);
+            }
+        };
+        getInstrumentation().getTargetContext().registerReceiver(
+                receiver, new IntentFilter(ACTION_USB_PERMISSION));
+        mUsbManager.requestPermission(device, PendingIntent.getBroadcast(
+                getInstrumentation().getTargetContext(),
+                0 /* requstCode */,
+                new Intent(ACTION_USB_PERMISSION),
+                0 /* flags */));
+        latch.await();
+        assertTrue(mUsbManager.hasPermission(device));
     }
 
     private UsbDevice findDevice() throws InterruptedException {
-        final UsbManager usbManager = getContext().getSystemService(UsbManager.class);
         while (true) {
-            final HashMap<String,UsbDevice> devices = usbManager.getDeviceList();
+            final HashMap<String,UsbDevice> devices = mUsbManager.getDeviceList();
             if (devices.size() == 0) {
                 show("Wait for devices.");
                 Thread.sleep(1000);
                 continue;
             }
             final UsbDevice device = devices.values().iterator().next();
-            final UsbDeviceConnection connection = usbManager.openDevice(device);
+            requestPermission(device);
+            final UsbDeviceConnection connection = mUsbManager.openDevice(device);
+            if (connection == null) {
+                fail("Cannot open USB connection.");
+            }
             for (int i = 0; i < device.getInterfaceCount(); i++) {
                 // Since the test runs real environment, we need to call claim interface with
                 // force = true to rob interfaces from other applications.
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/RealDeviceTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/RealDeviceTest.java
index 9641ad7..22daaf2 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/RealDeviceTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/RealDeviceTest.java
@@ -17,7 +17,10 @@
 package com.android.mtp;
 
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
-@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
 @interface RealDeviceTest {}
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestResultInstrumentation.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestResultInstrumentation.java
index 9824d28..a243375 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestResultInstrumentation.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestResultInstrumentation.java
@@ -45,8 +45,11 @@
     }
 
     private void show(String tag, Test test, Throwable t) {
+        String message = "";
+        if (t != null && t.getMessage() != null) {
+            message = t.getMessage();
+        }
         TestResultActivity.show(
-                getContext(),
-                String.format("[%s] %s %s", tag, test.toString(), t != null ? t.getMessage() : ""));
+                getContext(), String.format("[%s] %s %s", tag, test.toString(), message));
     }
 }