reader/android: use Syncbase blobs to store and sync pdf files.

Now the Syncbase blobs are used for storing pdf files, instead of
temporary local files. The pdf files are synchronizing across multiple
devices well.

Closes #33.

Change-Id: I7dae804b5f9264af4b4fc3494597f975a4da0c60
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 95309c2..388413f 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
@@ -14,6 +14,7 @@
 import android.util.Log;
 import android.view.View;
 import android.widget.Button;
+import android.widget.Toast;
 
 import com.google.common.io.ByteStreams;
 
@@ -173,14 +174,14 @@
 
     private void createAndJoinDeviceSet(Uri fileUri) {
         // Get the file content.
-        java.io.File jFile = getFileFromUri(fileUri);
-        if (jFile == null) {
-            Log.e(TAG, "Could not get the file content of Uri: " + fileUri.toString());
-            return;
-        }
+        byte[] bytes = getBytesFromUri(fileUri);
 
         // Create a vdl File object representing this pdf file and put it in the db.
-        File vFile = createVdlFile(jFile, fileUri);
+        File vFile = mDB.storeBytes(bytes, getTitleFromUri(fileUri));
+        Log.i(TAG, "vFile created: " + vFile);
+        if (vFile == null) {
+            Log.e(TAG, "Could not store the file content of Uri: " + fileUri.toString());
+        }
         mDB.addFile(vFile);
 
         // Create a device set object and put it in the db.
@@ -202,15 +203,6 @@
         leaveDeviceSet();
     }
 
-    private File createVdlFile(java.io.File jFile, Uri uri) {
-        String id = jFile.getName();
-        String title = getTitleFromUri(uri);
-        long size = jFile.length();
-        String type = Constants.PDF_MIME_TYPE;
-
-        return new File(id, null, title, size, type);
-    }
-
     private DeviceMeta createDeviceMeta() {
         String deviceId = DeviceInfoFactory.getDeviceId(this);
         int page = 1;
@@ -229,8 +221,23 @@
     }
 
     private void joinDeviceSet(DeviceSet ds) {
-        // TODO(youngseokyoon): use the blobref instead.
+        // Get the file contents from the database
+        // TODO(youngseokyoon): get the blob asynchronously. right now, it's blocking the UI thread.
+        File file = mDB.getFileList().getItemById(ds.getFileId());
+        byte[] bytes = mDB.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.
         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.
         // TODO(youngseokyoon): enable swipe and handle the page change events.
@@ -266,37 +273,12 @@
         mCurrentDS = null;
     }
 
-    private java.io.File getFileFromUri(Uri uri) {
-        Log.i(TAG, "File Uri: " + uri.toString());
+    private byte[] getBytesFromUri(Uri uri) {
+        Log.i(TAG, "getBytesFromUri: " + uri.toString());
 
         try (InputStream in = getContentResolver().openInputStream(uri)) {
             // Get the entire file contents as a byte array.
-            byte[] bytes = ByteStreams.toByteArray(in);
-
-            // Write the contents in a temporary file.
-            // For now, use the md5 hash string of the file as the filename.
-            // TODO(youngseokyoon): use the Syncbase blob to store the file.
-
-            String fileKey = IdFactory.getFileId(bytes);
-            if (fileKey == null) {
-                fileKey = IdFactory.getRandomId();
-                Log.w(TAG, "Could not get the MD5 hash string for Uri: " + uri.toString());
-                Log.w(TAG, "- Using a random UUID instead.");
-            }
-            Log.i(TAG, "FileKey: " + fileKey);
-
-            java.io.File jFile = new java.io.File(getCacheDir(), fileKey);
-            if (jFile.exists() && jFile.length() == bytes.length) {
-                Log.i(TAG, "The file already exists in the cache directory.");
-                return jFile;
-            }
-
-            Log.i(TAG, "Creating pdf file: " + jFile.getPath());
-            try (FileOutputStream out = new FileOutputStream(jFile)) {
-                out.write(bytes);
-            }
-
-            return jFile;
+            return ByteStreams.toByteArray(in);
         } catch (IOException e) {
             handleException(e);
         }
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 703f0bc..582c304 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
@@ -152,4 +152,24 @@
      */
     void deleteDeviceSet(String id);
 
+    /**
+     * Stores the given bytes and returns a File object representing the written data, which can be
+     * passed to readBytes() method to read the data back.
+     *
+     * The returned File object should be explicitly added to the database by calling addFile().
+     *
+     * @param bytes bytes to be written.
+     * @param title title of this file.
+     * @return      a File object representing the written data.
+     */
+    File storeBytes(byte[] bytes, String title);
+
+    /**
+     * Reads the bytes from the File object.
+     *
+     * @param file the file to be read.
+     * @return     the file contents as a byte array.
+     */
+    byte[] readBytes(File file);
+
 }
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 e9a9443..43c83b5 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
@@ -7,11 +7,19 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
+import android.util.Log;
 
+import com.google.common.io.ByteStreams;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
+import io.v.android.apps.reader.Constants;
 import io.v.android.apps.reader.model.DeviceInfoFactory;
+import io.v.android.apps.reader.model.IdFactory;
 import io.v.android.apps.reader.model.Listener;
 import io.v.android.apps.reader.vdl.Device;
 import io.v.android.apps.reader.vdl.DeviceSet;
@@ -22,11 +30,15 @@
  */
 public class FakeDB implements DB {
 
+    private static final String TAG = FakeDB.class.getSimpleName();
+
+    private Context mContext;
     private FakeFileList mFileList;
     private FakeDeviceList mDeviceList;
     private FakeDeviceSetList mDeviceSetList;
 
     public FakeDB(Context context) {
+        mContext = context;
         mFileList = new FakeFileList();
         mDeviceList = new FakeDeviceList();
         mDeviceSetList = new FakeDeviceSetList();
@@ -187,4 +199,37 @@
     public void deleteDeviceSet(String id) {
         mDeviceSetList.removeItemById(id);
     }
+
+    @Override
+    public File storeBytes(byte[] bytes, String title) {
+        // In Fake DB, store the bytes as a temporary file in the local filesystem.
+        String id = IdFactory.getFileId(bytes);
+
+        java.io.File jFile = new java.io.File(mContext.getCacheDir(), id);
+        try (FileOutputStream out = new FileOutputStream(jFile)) {
+            out.write(bytes);
+        } catch (IOException e) {
+            Log.e(TAG, e.getMessage(), e);
+        }
+
+        return new File(
+                id,
+                null,
+                title,
+                bytes.length,
+                Constants.PDF_MIME_TYPE
+        );
+    }
+
+    @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);
+        } 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 0dd0e2a..38628a6 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
@@ -15,12 +15,16 @@
 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 java.io.IOException;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
 import io.v.android.apps.reader.model.DeviceInfoFactory;
+import io.v.android.apps.reader.model.IdFactory;
 import io.v.android.apps.reader.model.Listener;
 import io.v.android.apps.reader.vdl.Device;
 import io.v.android.apps.reader.vdl.DeviceSet;
@@ -44,6 +48,7 @@
 import io.v.v23.security.access.Constants;
 import io.v.v23.security.access.Permissions;
 import io.v.v23.services.syncbase.nosql.BatchOptions;
+import io.v.v23.services.syncbase.nosql.BlobRef;
 import io.v.v23.services.syncbase.nosql.KeyValue;
 import io.v.v23.services.syncbase.nosql.SyncgroupMemberInfo;
 import io.v.v23.services.syncbase.nosql.SyncgroupSpec;
@@ -53,6 +58,8 @@
 import io.v.v23.syncbase.SyncbaseApp;
 import io.v.v23.syncbase.SyncbaseService;
 import io.v.v23.syncbase.nosql.BatchDatabase;
+import io.v.v23.syncbase.nosql.BlobReader;
+import io.v.v23.syncbase.nosql.BlobWriter;
 import io.v.v23.syncbase.nosql.Database;
 import io.v.v23.syncbase.nosql.RowRange;
 import io.v.v23.syncbase.nosql.Syncgroup;
@@ -486,6 +493,51 @@
         }
     }
 
+    @Override
+    public File storeBytes(byte[] bytes, String title) {
+        // In case of Syncbase DB, store the bytes as a blob.
+        // TODO(youngseokyoon): check if the same blob is already in the database.
+        try {
+            BlobWriter writer = mLocalSB.db.writeBlob(mVContext, null);
+            OutputStream out = writer.stream(mVContext);
+            out.write(bytes);
+            out.close();
+
+            writer.commit(mVContext);
+
+            BlobRef ref = writer.getRef();
+
+            return new File(
+                    IdFactory.getFileId(bytes),
+                    ref,
+                    title,
+                    bytes.length,
+                    io.v.android.apps.reader.Constants.PDF_MIME_TYPE
+            );
+        } catch (VException | IOException e) {
+            handleError("Could not write the blob: " + e.getMessage());
+        }
+
+        return null;
+    }
+
+    @Override
+    public byte[] readBytes(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()
+                    + ": " + e.getMessage());
+        }
+
+        return null;
+    }
+
     private void handleError(String msg) {
         Log.e(TAG, msg);
         Toast.makeText(mContext, msg, Toast.LENGTH_LONG).show();