Let apps provide a custom data source for extractors

Adds android.media.DataSource, which is modeled after its native namesake,
and a new method on MediaExtractor that lets apps specify their implementation
of a DataSource as the source of data for the extractor.

Change-Id: If1b169bd18d2691ebc4f8996494dfc8ee0894b6c
diff --git a/media/java/android/media/DataSource.java b/media/java/android/media/DataSource.java
new file mode 100644
index 0000000..347bd5f
--- /dev/null
+++ b/media/java/android/media/DataSource.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012 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.media;
+
+import java.io.Closeable;
+
+/**
+ * An abstraction for a media data source, e.g. a file or an http stream
+ * {@hide}
+ */
+public interface DataSource extends Closeable {
+    /**
+     * Reads data from the data source at the requested position
+     *
+     * @param offset where in the source to read
+     * @param buffer the buffer to read the data into
+     * @param size how many bytes to read
+     * @return the number of bytes read, or -1 if there was an error
+     */
+    public int readAt(long offset, byte[] buffer, int size);
+
+    /**
+     * Gets the size of the data source.
+     *
+     * @return size of data source, or -1 if the length is unknown
+     */
+    public long getSize();
+}
diff --git a/media/java/android/media/MediaExtractor.java b/media/java/android/media/MediaExtractor.java
index 687d3a5..4b8d3cb 100644
--- a/media/java/android/media/MediaExtractor.java
+++ b/media/java/android/media/MediaExtractor.java
@@ -22,6 +22,7 @@
 import android.media.MediaCodec;
 import android.media.MediaFormat;
 import android.net.Uri;
+
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -60,6 +61,12 @@
     }
 
     /**
+     * Sets the DataSource object to be used as the data source for this extractor
+     * {@hide}
+     */
+    public native final void setDataSource(DataSource source);
+
+    /**
      * Sets the data source as a content Uri.
      *
      * @param context the Context to use when resolving the Uri
diff --git a/media/jni/android_media_MediaExtractor.cpp b/media/jni/android_media_MediaExtractor.cpp
index 351ff04..23949fa 100644
--- a/media/jni/android_media_MediaExtractor.cpp
+++ b/media/jni/android_media_MediaExtractor.cpp
@@ -44,6 +44,72 @@
 
 static fields_t gFields;
 
+class JavaDataSourceBridge : public DataSource {
+    jmethodID mReadMethod;
+    jmethodID mGetSizeMethod;
+    jmethodID mCloseMethod;
+    jobject   mDataSource;
+ public:
+    JavaDataSourceBridge(JNIEnv *env, jobject source) {
+        mDataSource = env->NewGlobalRef(source);
+
+        jclass datasourceclass = env->GetObjectClass(mDataSource);
+        CHECK(datasourceclass != NULL);
+
+        mReadMethod = env->GetMethodID(datasourceclass, "readAt", "(J[BI)I");
+        CHECK(mReadMethod != NULL);
+
+        mGetSizeMethod = env->GetMethodID(datasourceclass, "getSize", "()J");
+        CHECK(mGetSizeMethod != NULL);
+
+        mCloseMethod = env->GetMethodID(datasourceclass, "close", "()V");
+        CHECK(mCloseMethod != NULL);
+    }
+
+    ~JavaDataSourceBridge() {
+        JNIEnv *env = AndroidRuntime::getJNIEnv();
+        env->CallVoidMethod(mDataSource, mCloseMethod);
+        env->DeleteGlobalRef(mDataSource);
+    }
+
+    virtual status_t initCheck() const {
+        return OK;
+    }
+
+    virtual ssize_t readAt(off64_t offset, void* buffer, size_t size) {
+        JNIEnv *env = AndroidRuntime::getJNIEnv();
+
+        // XXX could optimize this by reusing the same array
+        jbyteArray byteArrayObj = env->NewByteArray(size);
+        env->DeleteLocalRef(env->GetObjectClass(mDataSource));
+        env->DeleteLocalRef(env->GetObjectClass(byteArrayObj));
+        ssize_t numread = env->CallIntMethod(mDataSource, mReadMethod, offset, byteArrayObj, size);
+        env->GetByteArrayRegion(byteArrayObj, 0, size, (jbyte*) buffer);
+        env->DeleteLocalRef(byteArrayObj);
+        if (env->ExceptionCheck()) {
+            ALOGW("Exception occurred while reading %d at %lld", size, offset);
+            LOGW_EX(env);
+            env->ExceptionClear();
+            return -1;
+        }
+        return numread;
+    }
+
+    virtual status_t getSize(off64_t *size) {
+        JNIEnv *env = AndroidRuntime::getJNIEnv();
+
+        CHECK(size != NULL);
+
+        int64_t len = env->CallLongMethod(mDataSource, mGetSizeMethod);
+        if (len < 0) {
+            *size = ERROR_UNSUPPORTED;
+        } else {
+            *size = len;
+        }
+        return OK;
+    }
+};
+
 ////////////////////////////////////////////////////////////////////////////////
 
 JMediaExtractor::JMediaExtractor(JNIEnv *env, jobject thiz)
@@ -76,6 +142,10 @@
     return mImpl->setDataSource(fd, offset, size);
 }
 
+status_t JMediaExtractor::setDataSource(const sp<DataSource> &datasource) {
+    return mImpl->setDataSource(datasource);
+}
+
 size_t JMediaExtractor::countTracks() const {
     return mImpl->countTracks();
 }
@@ -625,6 +695,33 @@
     }
 }
 
+static void android_media_MediaExtractor_setDataSourceCallback(
+        JNIEnv *env, jobject thiz,
+        jobject callbackObj) {
+    sp<JMediaExtractor> extractor = getMediaExtractor(env, thiz);
+
+    if (extractor == NULL) {
+        jniThrowException(env, "java/lang/IllegalStateException", NULL);
+        return;
+    }
+
+    if (callbackObj == NULL) {
+        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
+        return;
+    }
+
+    sp<JavaDataSourceBridge> bridge = new JavaDataSourceBridge(env, callbackObj);
+    status_t err = extractor->setDataSource(bridge);
+
+    if (err != OK) {
+        jniThrowException(
+                env,
+                "java/io/IOException",
+                "Failed to instantiate extractor.");
+        return;
+    }
+}
+
 static jlong android_media_MediaExtractor_getCachedDurationUs(
         JNIEnv *env, jobject thiz) {
     sp<JMediaExtractor> extractor = getMediaExtractor(env, thiz);
@@ -713,6 +810,9 @@
     { "setDataSource", "(Ljava/io/FileDescriptor;JJ)V",
       (void *)android_media_MediaExtractor_setDataSourceFd },
 
+    { "setDataSource", "(Landroid/media/DataSource;)V",
+      (void *)android_media_MediaExtractor_setDataSourceCallback },
+
     { "getCachedDuration", "()J",
       (void *)android_media_MediaExtractor_getCachedDurationUs },
 
diff --git a/media/jni/android_media_MediaExtractor.h b/media/jni/android_media_MediaExtractor.h
index 2d4627e..03900db 100644
--- a/media/jni/android_media_MediaExtractor.h
+++ b/media/jni/android_media_MediaExtractor.h
@@ -19,6 +19,7 @@
 
 #include <media/stagefright/foundation/ABase.h>
 #include <media/stagefright/MediaSource.h>
+#include <media/stagefright/DataSource.h>
 #include <utils/Errors.h>
 #include <utils/KeyedVector.h>
 #include <utils/RefBase.h>
@@ -39,6 +40,7 @@
             const KeyedVector<String8, String8> *headers);
 
     status_t setDataSource(int fd, off64_t offset, off64_t size);
+    status_t setDataSource(const sp<DataSource> &source);
 
     size_t countTracks() const;
     status_t getTrackFormat(size_t index, jobject *format) const;