reader/android: pass the blob stream directly to pdf.js

Instead of creating a local cache file every time, the blob reader
stream is passed directly to pdf.js.

Also fixes some async logic bug in PdfViewWrapper with SettableFuture.

Change-Id: I519400632e46539ebfbed6a0b00c3581501171ca
diff --git a/android/app/src/main/java/io/v/android/apps/reader/PdfViewWrapper.java b/android/app/src/main/java/io/v/android/apps/reader/PdfViewWrapper.java
index 7ceb503..3a6ee81 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/PdfViewWrapper.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/PdfViewWrapper.java
@@ -15,11 +15,14 @@
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.SettableFuture;
+
 import java.io.InputStream;
 
+import io.v.android.apps.reader.db.DB;
+
 /**
  * Wrapper class for the PDF Viewer library.
  *
@@ -29,40 +32,21 @@
 
     private static final String TAG = PdfViewWrapper.class.getSimpleName();
 
+    private SettableFuture<Boolean> mPageLoaded;
     private int mPageCount;
 
     public PdfViewWrapper(Context context, AttributeSet attrs) {
         super(context, attrs);
+
+        mPageLoaded = SettableFuture.create();
     }
 
     public void init() {
         WebSettings settings = getSettings();
         settings.setJavaScriptEnabled(true);
         settings.setAllowUniversalAccessFromFileURLs(true);
-
         setWebChromeClient(new WebChromeClient());
-
-        setWebViewClient(new WebViewClient() {
-            @Override
-            public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
-                Log.i(TAG, "shouldInterceptRequest called");
-
-                File file = new File(request.getUrl().getPath());
-                Log.i(TAG, "file path: " + file.getPath());
-
-                try {
-                    // Should NOT close the stream here, so that the stream can be read by WebView.
-                    InputStream inputStream = new FileInputStream(file);
-
-                    Log.i(TAG, "returning a custom WebResourceResponse");
-                    return new WebResourceResponse("application/pdf", "binary", inputStream);
-                } catch (IOException e) {
-                    Log.i(TAG, "falling back to super.shouldInterceptRequest");
-                    return super.shouldInterceptRequest(view, request);
-                }
-            }
-        });
-
+        setWebViewClient(new PdfViewClient());
         addJavascriptInterface(new JSInterface(), "android");
 
         loadUrl("file:///android_asset/pdfjs/pdf-web-view.html");
@@ -70,13 +54,23 @@
 
     /**
      * Loads the PDF file at the given path into the pdf.js component within WebView.
-     * NOTE: must be called after the page loading is finished.
      */
-    public void loadPdfFile(String filePath) {
-        evaluateJavascript("window.client.open(\"" + filePath + "\");", null);
+    public void loadPdfFile(final String filePath) {
+        Futures.addCallback(mPageLoaded, new FutureCallback<Boolean>() {
+            @Override
+            public void onSuccess(Boolean result) {
+                Log.i(TAG, "loadPdfFile called: " + filePath);
+                evaluateJavascript("window.client.open(\"" + filePath + "\");", null);
 
-        // leave the page count as 0 until the page count value is properly set from JS side.
-        mPageCount = 0;
+                // leave the page count as 0 until the page count value is properly set from JS side.
+                mPageCount = 0;
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                // Nothing to do.
+            }
+        });
     }
 
     /**
@@ -106,4 +100,38 @@
         }
     }
 
+    private class PdfViewClient extends WebViewClient {
+        @Override
+        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
+            Log.i(TAG, "shouldInterceptRequest called");
+
+            String path = request.getUrl().getPath();
+            if (!path.startsWith("/file_id/")) {
+                Log.i(TAG, "Not a file id path. Falling back to super.shouldInterceptRequest");
+                return super.shouldInterceptRequest(view, request);
+            }
+
+            String fileId = request.getUrl().getLastPathSegment();
+            Log.i(TAG, "File ID: " + fileId);
+
+            // Should NOT close the stream here, so that the stream can be read by WebView.
+            InputStream in = DB.Singleton.get(getContext()).getInputStreamForFile(fileId);
+
+            if (in != null) {
+                Log.i(TAG, "returning a custom WebResourceResponse");
+                return new WebResourceResponse("application/pdf", "binary", in);
+            } else {
+                Log.i(TAG, "Could not open an input stream. " +
+                        "Falling back to super.shouldInterceptRequest");
+                return super.shouldInterceptRequest(view, request);
+            }
+        }
+
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            super.onPageFinished(view, url);
+            mPageLoaded.set(true);
+        }
+    }
+
 }
diff --git a/android/app/src/main/java/io/v/android/apps/reader/PdfViewerActivity.java b/android/app/src/main/java/io/v/android/apps/reader/PdfViewerActivity.java
index 2dfe9b7..ab57c61 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/PdfViewerActivity.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/PdfViewerActivity.java
@@ -16,14 +16,10 @@
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import android.widget.Toast;
 
 import com.google.android.gms.analytics.HitBuilders;
 import com.google.common.io.ByteStreams;
 
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashMap;
@@ -297,41 +293,12 @@
     }
 
     private void joinDeviceSet(DeviceSet ds) {
-        // Get the file contents from the database
-        // TODO(youngseokyoon): get the blob asynchronously. right now, it's blocking the UI thread.
-        File file = getDB().getFileList().getItemById(ds.getFileId());
-        byte[] bytes = getDB().readBytes(file);
-        if (bytes == null) {
-            Toast.makeText(this, "Could not load the file contents.", Toast.LENGTH_LONG).show();
-            return;
-        }
-
-        // The pdf viewer widget requires the file to be an actual java.io.File object.
-        // Create a temporary file and write the contents.
-        final java.io.File jFile = new java.io.File(getCacheDir(), ds.getFileId());
-        try (FileOutputStream out = new FileOutputStream(jFile)) {
-            out.write(bytes);
-        } catch (IOException e) {
-            handleException(e);
-        }
-
-        // Initialize the pdf viewer widget with the file content.
-        Log.i(TAG, "File path: " + jFile.getPath());
-
-        // TODO(youngseokyoon): move this logic to PdfViewWrapper
-        mPdfView.setWebViewClient(new WebViewClient() {
-            @Override
-            public void onPageFinished(WebView view, String url) {
-                super.onPageFinished(view, url);
-                mPdfView.loadPdfFile(jFile.getPath());
-
-                writeNavigationAction("Page Changed", 1);
-            }
-        });
+        mPdfView.loadPdfFile("/file_id/" + ds.getFileId());
 
         // Create a new device meta, and update the device set with it.
         Log.i(TAG, "Joining device set: " + ds.getId());
         DeviceMeta dm = createDeviceMeta();
+        // TODO(youngseokyoon): don't wait till these operations are finished.
         ds.getDevices().put(dm.getDeviceId(), dm);
         getDB().updateDeviceSet(ds);
 
diff --git a/android/app/src/main/java/io/v/android/apps/reader/db/DB.java b/android/app/src/main/java/io/v/android/apps/reader/db/DB.java
index 1dd7aad..28b0b69 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/db/DB.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/db/DB.java
@@ -7,6 +7,8 @@
 import android.app.Activity;
 import android.content.Context;
 
+import java.io.InputStream;
+
 import io.v.android.apps.reader.model.Listener;
 import io.v.android.apps.reader.vdl.Device;
 import io.v.android.apps.reader.vdl.DeviceSet;
@@ -156,11 +158,18 @@
     File storeBytes(byte[] bytes, String title);
 
     /**
-     * Reads the bytes from the File object.
+     * Opens an {@link InputStream} for the given file.
      *
      * @param file the file to be read.
-     * @return     the file contents as a byte array.
+     * @return     the input stream for the given file, or null if an error occurs.
      */
-    byte[] readBytes(File file);
+    InputStream getInputStreamForFile(File file);
+
+    /**
+     * Opens an {@link InputStream} for the given file id.
+     * @param fileId the id of the file to be read.
+     * @return       the input stream for the given file id, or null if an error occurs.
+     */
+    InputStream getInputStreamForFile(String fileId);
 
 }
diff --git a/android/app/src/main/java/io/v/android/apps/reader/db/FakeDB.java b/android/app/src/main/java/io/v/android/apps/reader/db/FakeDB.java
index e0e55a4..c7f2497 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/db/FakeDB.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/db/FakeDB.java
@@ -8,11 +8,10 @@
 import android.content.Context;
 import android.util.Log;
 
-import com.google.common.io.ByteStreams;
-
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -215,14 +214,20 @@
     }
 
     @Override
-    public byte[] readBytes(File file) {
-        java.io.File jFile = new java.io.File(mContext.getCacheDir(), file.getId());
-        try (FileInputStream in = new FileInputStream(jFile)) {
-            return ByteStreams.toByteArray(in);
+    public InputStream getInputStreamForFile(File file) {
+        return getInputStreamForFile(file.getId());
+    }
+
+    @Override
+    public InputStream getInputStreamForFile(String fileId) {
+        java.io.File jFile = new java.io.File(mContext.getCacheDir(), fileId);
+        try {
+            return new FileInputStream(jFile);
         } catch (IOException e) {
             Log.e(TAG, e.getMessage(), e);
         }
 
         return null;
     }
+
 }
diff --git a/android/app/src/main/java/io/v/android/apps/reader/db/SyncbaseDB.java b/android/app/src/main/java/io/v/android/apps/reader/db/SyncbaseDB.java
index 6959473..6216a39 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/db/SyncbaseDB.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/db/SyncbaseDB.java
@@ -14,7 +14,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
-import com.google.common.io.ByteStreams;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -22,6 +21,7 @@
 import org.apache.commons.io.FileUtils;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.List;
@@ -518,22 +518,32 @@
     }
 
     @Override
-    public byte[] readBytes(File file) {
+    public InputStream getInputStreamForFile(File file) {
         if (file == null || file.getRef() == null) {
             return null;
         }
 
         try {
             BlobReader reader = mLocalSB.db.readBlob(mVContext, file.getRef());
-            return ByteStreams.toByteArray(reader.stream(mVContext, 0L));
-        } catch (VException | IOException e) {
-            handleError("Could not read the blob " + file.getRef().toString()
+            return reader.stream(mVContext, 0L);
+        } catch (VException e) {
+            handleError("Could not open the input stream for file " + file.getRef().toString()
                     + ": " + e.getMessage());
         }
 
         return null;
     }
 
+    @Override
+    public InputStream getInputStreamForFile(String fileId) {
+        try {
+            File file = (File) sync(mLocalSB.files.get(mVContext, fileId, File.class));
+            return getInputStreamForFile(file);
+        } catch (VException e) {
+            return null;
+        }
+    }
+
     private void handleError(String msg) {
         Log.e(TAG, msg);
         Toast.makeText(mContext, msg, Toast.LENGTH_LONG).show();