reader/android: add floating action button for creating a new device set

Adds a floating action button (FAB) to DeviceSetChooser activity.
When the FAB is touched, the user is directed to a file chooser
activity. Once a pdf file is chosen, the app creates the corresponding
vdl objects and put them in the Syncbase tables.

The previously baked-in PDF files are also removed.

Synchronization works between multiple Android devices, and pages are
always linked by default for now.

NOTE: Not yet using the Syncbase blobs. All the devices need to have
the same pdf files on their local storage.

Change-Id: I1bfeae26f58146e1e9818544e07aea5268948260
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 590fd3d..05f52d2 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -70,8 +70,10 @@
 
 dependencies {
     compile fileTree(dir: 'libs', include: ['*.jar'])
-    compile 'com.android.support:cardview-v7:22+'
-    compile 'com.android.support:recyclerview-v7:22+'
+    compile 'com.android.support:design:22.2.1'
+    compile 'com.android.support:appcompat-v7:22.2.1'
+    compile 'com.android.support:cardview-v7:22.2.1'
+    compile 'com.android.support:recyclerview-v7:22.2.1'
     compile 'com.joanzapata.pdfview:android-pdfview:1.0.4@aar'
     compile project(':android-lib')
 }
diff --git a/android/app/src/main/assets/Bar.pdf b/android/app/src/main/assets/Bar.pdf
deleted file mode 100644
index 8fa091b..0000000
--- a/android/app/src/main/assets/Bar.pdf
+++ /dev/null
Binary files differ
diff --git a/android/app/src/main/assets/Foo.pdf b/android/app/src/main/assets/Foo.pdf
deleted file mode 100644
index 9e2cb1e..0000000
--- a/android/app/src/main/assets/Foo.pdf
+++ /dev/null
Binary files differ
diff --git a/android/app/src/main/java/io/v/android/apps/reader/Constants.java b/android/app/src/main/java/io/v/android/apps/reader/Constants.java
new file mode 100644
index 0000000..26e27a4
--- /dev/null
+++ b/android/app/src/main/java/io/v/android/apps/reader/Constants.java
@@ -0,0 +1,11 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.apps.reader;
+
+public class Constants {
+
+    public static final String PDF_MIME_TYPE = "application/pdf";
+
+}
diff --git a/android/app/src/main/java/io/v/android/apps/reader/DeviceSetChooserActivity.java b/android/app/src/main/java/io/v/android/apps/reader/DeviceSetChooserActivity.java
index b6953a1..27f4598 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/DeviceSetChooserActivity.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/DeviceSetChooserActivity.java
@@ -6,22 +6,18 @@
 
 import android.app.Activity;
 import android.content.Intent;
+import android.net.Uri;
 import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.helper.ItemTouchHelper;
+import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.widget.Button;
-
-import com.google.common.collect.ImmutableMap;
-
-import java.util.UUID;
 
 import io.v.android.apps.reader.db.DB;
-import io.v.android.apps.reader.vdl.DeviceMeta;
-import io.v.android.apps.reader.vdl.DeviceSet;
 
 /**
  * Activity that displays all the active device sets of this user.
@@ -31,9 +27,13 @@
  */
 public class DeviceSetChooserActivity extends Activity {
 
+    private static final String TAG = DeviceSetChooserActivity.class.getSimpleName();
+
+    private static final int CHOOSE_PDF_FILE_REQUEST = 300;
+
     private RecyclerView mRecyclerView;
     private DeviceSetListAdapter mAdapter;
-    private Button mButtonAddDeviceSet;
+    private FloatingActionButton mButtonAddDeviceSet;
     private DB mDB;
 
     protected void onCreate(Bundle savedInstanceState) {
@@ -52,18 +52,16 @@
         RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
         mRecyclerView.setLayoutManager(layoutManager);
 
-        // "Add Device Set" button initialization
-        mButtonAddDeviceSet = (Button) findViewById(R.id.button_add_device_set);
+        // Add device set FAB initialization
+        mButtonAddDeviceSet = (FloatingActionButton) findViewById(R.id.button_add_device_set);
         mButtonAddDeviceSet.setOnClickListener(new View.OnClickListener() {
             @Override
-            public void onClick(View v) {
-                // Create a new device set and add it to the database.
-                DeviceSet ds = new DeviceSet(
-                        UUID.randomUUID().toString(),              // Device Set ID
-                        UUID.randomUUID().toString(),              // File ID
-                        ImmutableMap.<String, DeviceMeta>of());
-
-                mDB.addDeviceSet(ds);
+            public void onClick(View view) {
+                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+                intent.setType(Constants.PDF_MIME_TYPE);
+                if (intent.resolveActivity(getPackageManager()) != null) {
+                    startActivityForResult(intent, CHOOSE_PDF_FILE_REQUEST);
+                }
             }
         });
     }
@@ -81,7 +79,7 @@
             public void onDeviceSetClick(DeviceSetListAdapter adapter, View v, int position) {
                 Intent intent = PdfViewerActivity.createIntent(
                         getApplicationContext(),
-                        adapter.getItemTitle(position));
+                        adapter.getDeviceSetId(position));
                 startActivity(intent);
             }
         });
@@ -145,9 +143,25 @@
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
+
+        Log.i(TAG, String.format("onActivityResult(%d, %d, data) called", requestCode, resultCode));
         if (mDB.onActivityResult(requestCode, resultCode, data)) {
             return;
         }
+
         // Any other activity results would be handled here.
+        if (requestCode == CHOOSE_PDF_FILE_REQUEST) {
+            if (resultCode == RESULT_OK) {
+                Uri fullPdfUri = data.getData();
+                Log.i(TAG, "Uri of the provided PDF: " + fullPdfUri);
+
+                Intent intent = PdfViewerActivity.createIntent(this, fullPdfUri);
+                startActivity(intent);
+            }
+        } else {
+            Log.w(TAG, String.format(
+                    "Unhandled activity result. (requestCode: %d, resultCode: %d)",
+                    requestCode, resultCode));
+        }
     }
 }
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 e9af493..2ea4544 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
@@ -21,27 +21,12 @@
     }
 
     /**
-     * Move to the previous page, if the current page is not the first page.
+     * Jumps to the given page number. Page number is one-based.
+     *
+     * @param page the page number to jump to. Page number is one-based.
      */
-    public void prevPage() {
-        // NOTE: getCurrentPage() returns a zero-based page number,
-        //       whereas the jumpTo() method expects a one-based page number.
-        int page = getCurrentPage();
-        if (page > 0) {
-            jumpTo(page);
-        }
-    }
-
-    /**
-     * Move to the next page, if the current page is not the last page.
-     */
-    public void nextPage() {
-        // NOTE: getCurrentPage() returns a zero-based page number,
-        //       whereas the jumpTo() method expects a one-based page number.
-        int page = getCurrentPage();
-        if (page < getPageCount() - 1) {
-            jumpTo(page + 2);
-        }
+    public void setPage(int page) {
+        jumpTo(page);
     }
 
 }
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 9c00973..d7deae1 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
@@ -7,61 +7,420 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
 import android.os.Bundle;
+import android.provider.OpenableColumns;
+import android.util.Log;
 import android.view.View;
 import android.widget.Button;
 
+import com.google.common.io.ByteStreams;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import io.v.android.apps.reader.db.DB;
+import io.v.android.apps.reader.db.DB.DBList;
+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.DeviceMeta;
+import io.v.android.apps.reader.vdl.DeviceSet;
+import io.v.android.apps.reader.vdl.File;
+
 /**
  * Activity that shows the contents of the selected pdf file.
  */
 public class PdfViewerActivity extends Activity {
 
-    private static final String EXTRA_FILE_NAME = "file_name";
+    private static final String TAG = PdfViewerActivity.class.getSimpleName();
+
+    private static final String EXTRA_DEVICE_SET_ID = "device_set_id";
 
     private PdfViewWrapper mPdfView;
-
     private Button mButtonPrev;
     private Button mButtonNext;
 
+    private DB mDB;
+    private DBList<DeviceSet> mDeviceSets;
+    private DeviceSet mCurrentDS;
+
     /**
-     * Helper method for creating an intent to start a PdfViewerActivity.
+     * Helper methods for creating an intent to start a PdfViewerActivity.
      */
-    public static Intent createIntent(Context context, String fileName) {
+    public static Intent createIntent(Context context, String deviceSetId) {
         Intent intent = new Intent(context, PdfViewerActivity.class);
-        intent.putExtra(EXTRA_FILE_NAME, fileName);
+        intent.putExtra(EXTRA_DEVICE_SET_ID, deviceSetId);
+        return intent;
+    }
+
+    public static Intent createIntent(Context context, Uri uri) {
+        Intent intent = new Intent(context, PdfViewerActivity.class);
+        intent.setData(uri);
         return intent;
     }
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        // Initialize the DB
+        mDB = DB.Singleton.get(this);
+        mDB.init(this);
+
         setContentView(R.layout.activity_pdf_viewer);
 
         mPdfView = (PdfViewWrapper) findViewById(R.id.pdfview);
 
-        // Load the pdf file using the file name passed with the intent.
-        Intent intent = getIntent();
-        if (intent.hasExtra(EXTRA_FILE_NAME)) {
-            mPdfView.fromAsset(intent.getStringExtra(EXTRA_FILE_NAME))
-                    .enableSwipe(true)
-                    .load();
-        }
-
         mButtonPrev = (Button) findViewById(R.id.button_prev);
         mButtonNext = (Button) findViewById(R.id.button_next);
 
         mButtonPrev.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                mPdfView.prevPage();
+                prevPage();
             }
         });
 
         mButtonNext.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                mPdfView.nextPage();
+                nextPage();
             }
         });
     }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+
+        mDeviceSets = mDB.getDeviceSetList();
+        mDeviceSets.setListener(new Listener() {
+            @Override
+            public void notifyItemChanged(int position) {
+                if (mCurrentDS == null) {
+                    return;
+                }
+
+                DeviceSet changed = mDeviceSets.getItem(position);
+                if (!changed.getId().equals(mCurrentDS.getId())) {
+                    return;
+                }
+
+                mCurrentDS = changed;
+                mPdfView.setPage(getDeviceMeta().getPage());
+            }
+
+            @Override
+            public void notifyItemInserted(int position) {
+                // Nothing to do
+            }
+
+            @Override
+            public void notifyItemRemoved(int position) {
+                // Nothing to do
+            }
+        });
+
+        Intent intent = getIntent();
+
+        if (intent.hasExtra(EXTRA_DEVICE_SET_ID)) {
+            /**
+             * Case #1.
+             * The EXTRA_DEVICE_SET_ID value is set when this activity is started by touching one of
+             * the existing device sets from the DeviceSetChooserActivity.
+             */
+
+            // Get the device set from the DB and join it.
+            DeviceSet ds = mDeviceSets.getItemById(intent.getStringExtra(EXTRA_DEVICE_SET_ID));
+            joinDeviceSet(ds);
+        } else if (intent.getData() != null) {
+            /**
+             * Case #2.
+             * The intent.getData() is set as a content Uri when this activity is started by using
+             * the floating action button from the DeviceSetChooserActivity and selecting one of the
+             * local PDF files from the browser.
+             */
+            // Get the file content.
+            java.io.File jFile = getFileFromUri(intent.getData());
+            if (jFile == null) {
+                Log.e(TAG, "Could not get the file content of Uri: " + intent.getData().toString());
+                return;
+            }
+
+            // Create a vdl File object representing this pdf file and put it in the db.
+            File vFile = createVdlFile(jFile, intent.getData());
+            mDB.addFile(vFile);
+
+            // Create a device set object and put it in the db.
+            DeviceSet ds = createDeviceSet(vFile);
+            mDB.addDeviceSet(ds);
+
+            // Join the device set.
+            joinDeviceSet(ds);
+        }
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+
+        if (mDeviceSets != null) {
+            mDeviceSets.discard();
+        }
+
+        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;
+        int zoom = 1;
+        boolean linked = true;
+
+        return new DeviceMeta(deviceId, page, zoom, linked);
+    }
+
+    private DeviceSet createDeviceSet(File vFile) {
+        String id = IdFactory.getRandomId();
+        String fileId = vFile.getId();
+        Map<String, DeviceMeta> devices = new HashMap<>();
+
+        return new DeviceSet(id, fileId, devices);
+    }
+
+    private void joinDeviceSet(DeviceSet ds) {
+        // TODO(youngseokyoon): use the blobref instead.
+        java.io.File jFile = new java.io.File(getCacheDir(), ds.getFileId());
+
+        // Initialize the pdf viewer widget with the file content.
+        // TODO(youngseokyoon): enable swipe and handle the page change events.
+        mPdfView.fromFile(jFile)
+                .enableSwipe(false)
+                .load();
+
+        // Create a new device meta, and update the device set with it.
+        Log.i(TAG, "Joining device set: " + ds.getId());
+        DeviceMeta dm = createDeviceMeta();
+        ds.getDevices().put(dm.getDeviceId(), dm);
+        mDB.updateDeviceSet(ds);
+
+        mCurrentDS = ds;
+    }
+
+    private void leaveDeviceSet() {
+        if (mCurrentDS == null) {
+            return;
+        }
+
+        Log.i(TAG, "Leaving device set: " + mCurrentDS.getId());
+        Map<String, DeviceMeta> devices = mCurrentDS.getDevices();
+        devices.remove(DeviceInfoFactory.getDeviceId(this));
+
+        if (devices.isEmpty()) {
+            Log.i(TAG, "Last one to leave the device set. Deleting " + mCurrentDS.getId());
+            mDB.deleteDeviceSet(mCurrentDS.getId());
+        } else {
+            mDB.updateDeviceSet(mCurrentDS);
+        }
+
+        mCurrentDS = null;
+    }
+
+    private java.io.File getFileFromUri(Uri uri) {
+        Log.i(TAG, "File Uri: " + 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;
+        } catch (IOException e) {
+            handleException(e);
+        }
+
+        return null;
+    }
+
+    private String getTitleFromUri(Uri uri) {
+        try {
+            Cursor cursor = getContentResolver().query(uri, null, null, null, null);
+
+            int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+            cursor.moveToFirst();
+            return cursor.getString(nameIndex);
+        } catch (Exception e) {
+            handleException(e);
+
+            if (uri != null) {
+                return uri.getLastPathSegment();
+            }
+        }
+
+        return null;
+    }
+
+    private DeviceMeta getDeviceMeta() {
+        return getDeviceMeta(mCurrentDS);
+    }
+
+    private DeviceMeta getDeviceMeta(DeviceSet ds) {
+        String deviceId = DeviceInfoFactory.getDeviceId(this);
+
+        if (ds == null || !ds.getDevices().containsKey(deviceId)) {
+            return null;
+        }
+
+        return ds.getDevices().get(deviceId);
+    }
+
+    /**
+     * Move all the linked pages to their previous pages.
+     */
+    private void prevPage() {
+        if (mCurrentDS == null) {
+            return;
+        }
+
+        // First, check if this device is linked or not.
+        // If not, simply move the page of the current device.
+        if (!getDeviceMeta().getLinked()) {
+            DeviceMeta dm = getDeviceMeta();
+            if (dm.getPage() > 1) {
+                dm.setPage(dm.getPage() - 1);
+            }
+
+            return;
+        }
+
+        // Move all the linked pages
+        Map<String, DeviceMeta> linkedDevices = getLinkedDevices();
+        int smallestPage = getSmallestPage(linkedDevices);
+
+        if (smallestPage > 1) {
+            for (String deviceId : linkedDevices.keySet()) {
+                DeviceMeta dm = linkedDevices.get(deviceId);
+                dm.setPage(dm.getPage() - 1);
+            }
+
+            mDB.updateDeviceSet(mCurrentDS);
+        }
+    }
+
+    /**
+     * Move all the linked pages to their next pages.
+     */
+    private void nextPage() {
+        if (mCurrentDS == null) {
+            return;
+        }
+
+        // First, check if this device is linked or not.
+        // If not, simply move the page of the current device.
+        if (!getDeviceMeta().getLinked()) {
+            DeviceMeta dm = getDeviceMeta();
+            if (dm.getPage() < mPdfView.getPageCount()) {
+                dm.setPage(dm.getPage() + 1);
+            }
+
+            return;
+        }
+
+        // Move all the linked pages
+        Map<String, DeviceMeta> linkedDevices = getLinkedDevices();
+        int largestPage = getLargestPage(linkedDevices);
+
+        if (largestPage < mPdfView.getPageCount()) {
+            for (String deviceId : linkedDevices.keySet()) {
+                DeviceMeta dm = linkedDevices.get(deviceId);
+                dm.setPage(dm.getPage() + 1);
+            }
+
+            mDB.updateDeviceSet(mCurrentDS);
+        }
+    }
+
+    private Map<String, DeviceMeta> getLinkedDevices() {
+        if (mCurrentDS == null) {
+            return null;
+        }
+
+        Map<String, DeviceMeta> devices = mCurrentDS.getDevices();
+        Map<String, DeviceMeta> result = new HashMap<>();
+        for (String deviceId : devices.keySet()) {
+            DeviceMeta dm = devices.get(deviceId);
+            if (dm.getLinked()) {
+                result.put(deviceId, dm);
+            }
+        }
+
+        return result;
+    }
+
+    private int getSmallestPage(Map<String, DeviceMeta> devices) {
+        int result = -1;
+
+        for (String deviceId : devices.keySet()) {
+            DeviceMeta dm = devices.get(deviceId);
+            if (result == -1 || dm.getPage() < result) {
+                result = dm.getPage();
+            }
+        }
+
+        return result;
+    }
+
+    private int getLargestPage(Map<String, DeviceMeta> devices) {
+        int result = -1;
+
+        for (String deviceId : devices.keySet()) {
+            DeviceMeta dm = devices.get(deviceId);
+            if (result == -1 || dm.getPage() > result) {
+                result = dm.getPage();
+            }
+        }
+
+        return result;
+    }
+
+    private static void handleException(Exception e) {
+        Log.e(TAG, e.getMessage(), 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 c8e0bc3..eec476b 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
@@ -111,6 +111,20 @@
     DBList<DeviceSet> getDeviceSetList();
 
     /**
+     * Adds a new file to the db.
+     *
+     * @param file the file to be added.
+     */
+    void addFile(File file);
+
+    /**
+     * Deletes a file with the given id.
+     *
+     * @param id the id of the file.
+     */
+    void deleteFile(String id);
+
+    /**
      * Adds a new device set to the db.
      *
      * @param ds the device set to be added.
@@ -118,6 +132,13 @@
     void addDeviceSet(DeviceSet ds);
 
     /**
+     * Updates a device set in the db.
+     *
+     * @param ds the device set to be updated.
+     */
+    void updateDeviceSet(DeviceSet ds);
+
+    /**
      * Deletes a device set with the given id.
      *
      * @param id the id of the device set.
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 5af039d..f190d10 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,7 +7,6 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
-import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -23,29 +22,49 @@
  */
 public class FakeDB implements DB {
 
-    private static final Object[][] FILE_DATA = {
-            { "fileId1", "Foo.pdf" },
-            { "fileId2", "Bar.pdf" },
-    };
-
-    private static final Object[][] DEVICE_SET_DATA = {
-            { "deviceSetId1", "fileId1" },
-            { "deviceSetId2", "fileId2" },
-    };
-
     private FakeFileList mFileList;
     private FakeDeviceList mDeviceList;
     private FakeDeviceSetList mDeviceSetList;
 
     public FakeDB(Context context) {
         mFileList = new FakeFileList();
-        mDeviceList = new FakeDeviceList(context);
+        mDeviceList = new FakeDeviceList();
         mDeviceSetList = new FakeDeviceSetList();
+
+        mDeviceList.addItem(DeviceInfoFactory.getDevice(context));
     }
 
     static abstract class BaseFakeList<E> implements DBList<E> {
 
-        protected Listener mListener;
+        private List<E> mItems;
+        private Listener mListener;
+
+        public BaseFakeList() {
+            mItems = new ArrayList<>();
+        }
+
+        protected abstract String getId(E e);
+
+        @Override
+        public int getItemCount() {
+            return mItems.size();
+        }
+
+        @Override
+        public E getItem(int position) {
+            return mItems.get(position);
+        }
+
+        @Override
+        public E getItemById(String id) {
+            for (E item : mItems) {
+                if (getId(item).equals(id)) {
+                    return item;
+                }
+            }
+
+            return null;
+        }
 
         @Override
         public void setListener(Listener listener) {
@@ -58,138 +77,59 @@
             // Nothing to do.
         }
 
-    }
-
-    static class FakeFileList extends BaseFakeList<File> {
-
-        private List<File> mFiles;
-
-        public FakeFileList() {
-            mFiles = new ArrayList<>();
-            for (Object[] fileData : FILE_DATA) {
-                mFiles.add(createFile(fileData));
-            }
-        }
-
-        private static File createFile(Object[] fileData) {
-            return new File(
-                    (String) fileData[0],   // File ID
-                    null,                   // BlobRef
-                    (String) fileData[1],   // Title
-                    0L,                     // Size
-                    null                    // Type
-            );
-        }
-
-        @Override
-        public int getItemCount() {
-            return mFiles.size();
-        }
-
-        @Override
-        public File getItem(int position) {
-            return mFiles.get(position);
-        }
-
-        @Override
-        public File getItemById(String id) {
-            for (File file : mFiles) {
-                if (file.getId().equals(id)) {
-                    return file;
-                }
-            }
-
-            return null;
-        }
-    }
-
-    static class FakeDeviceList extends BaseFakeList<Device> {
-
-        private static final String TAG = FakeDeviceList.class.getSimpleName();
-
-        private Context mContext;
-
-        public FakeDeviceList(Context context) {
-            mContext = context;
-            Log.i(TAG, "Device Info: " + getItem(0));
-        }
-
-        @Override
-        public int getItemCount() {
-            return 1;
-        }
-
-        @Override
-        public Device getItem(int position) {
-            return DeviceInfoFactory.get(mContext);
-        }
-
-        @Override
-        public Device getItemById(String id) {
-            Device device = DeviceInfoFactory.get(mContext);
-            if (device.getId().equals(id)) {
-                return device;
-            }
-
-            return null;
-        }
-    }
-
-    static class FakeDeviceSetList extends BaseFakeList<DeviceSet> {
-
-        private List<DeviceSet> mDeviceSets;
-
-        public FakeDeviceSetList() {
-            mDeviceSets = new ArrayList<>();
-            for (Object[] deviceSetData : DEVICE_SET_DATA) {
-                mDeviceSets.add(createDeviceSet(deviceSetData));
-            }
-        }
-
-        private static DeviceSet createDeviceSet(Object[] deviceSetData) {
-            return new DeviceSet(
-                    (String) deviceSetData[0],  // Device Set ID
-                    (String) deviceSetData[1],  // File ID
-                    null                        // Devices
-            );
-        }
-
-        @Override
-        public int getItemCount() {
-            return mDeviceSets.size();
-        }
-
-        @Override
-        public DeviceSet getItem(int position) {
-            return mDeviceSets.get(position);
-        }
-
-        @Override
-        public DeviceSet getItemById(String id) {
-            for (DeviceSet ds : mDeviceSets) {
-                if (ds.getId().equals(id)) {
-                    return ds;
-                }
-            }
-
-            return null;
-        }
-
-        public void addItem(DeviceSet ds) {
-            mDeviceSets.add(ds);
+        public void addItem(E item) {
+            mItems.add(item);
             if (mListener != null) {
-                mListener.notifyItemInserted(mDeviceSets.size() - 1);
+                mListener.notifyItemInserted(mItems.size() - 1);
+            }
+        }
+
+        public void updateItem(E item) {
+            for (int i = 0; i < mItems.size(); ++i) {
+                if (getId(mItems.get(i)).equals(getId(item))) {
+                    mItems.set(i, item);
+
+                    if (mListener != null) {
+                        mListener.notifyItemChanged(i);
+                    }
+
+                    return;
+                }
             }
         }
 
         public void removeItemById(String id) {
-            for (int i = 0; i < mDeviceSets.size(); ++i) {
-                if (mDeviceSets.get(i).getId().equals(id)) {
-                    mDeviceSets.remove(i);
+            for (int i = 0; i < mItems.size(); ++i) {
+                if (getId(mItems.get(i)).equals(id)) {
+                    mItems.remove(i);
+
+                    if (mListener != null) {
+                        mListener.notifyItemRemoved(i);
+                    }
+
                     return;
                 }
             }
         }
+
+    }
+
+    static class FakeFileList extends BaseFakeList<File> {
+        public String getId(File file) {
+            return file.getId();
+        }
+    }
+
+    static class FakeDeviceList extends BaseFakeList<Device> {
+        public String getId(Device device) {
+            return device.getId();
+        }
+    }
+
+    static class FakeDeviceSetList extends BaseFakeList<DeviceSet> {
+        public String getId(DeviceSet ds) {
+            return ds.getId();
+        }
     }
 
     public void init(Activity activity) {
@@ -218,11 +158,26 @@
     }
 
     @Override
+    public void addFile(File file) {
+        mFileList.addItem(file);
+    }
+
+    @Override
+    public void deleteFile(String id) {
+        mFileList.removeItemById(id);
+    }
+
+    @Override
     public void addDeviceSet(DeviceSet ds) {
         mDeviceSetList.addItem(ds);
     }
 
     @Override
+    public void updateDeviceSet(DeviceSet ds) {
+        mDeviceSetList.updateItem(ds);
+    }
+
+    @Override
     public void deleteDeviceSet(String id) {
         mDeviceSetList.removeItemById(id);
     }
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 48b7712..f040dfb 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
@@ -187,7 +187,7 @@
                 "users",
                 mUsername,
                 "reader",
-                DeviceInfoFactory.get(mContext).getId(),
+                DeviceInfoFactory.getDevice(mContext).getId(),
                 "syncbase"
         );
         Log.i(TAG, "SyncbaseName: " + syncbaseName);
@@ -322,7 +322,7 @@
 
     private void registerDevice() {
         try {
-            Device thisDevice = DeviceInfoFactory.get(mContext);
+            Device thisDevice = DeviceInfoFactory.getDevice(mContext);
             mLocalSB.devices.put(mVContext, thisDevice.getId(), thisDevice, Device.class);
             Log.i(TAG, "Registered this device to the syncbase table: " + thisDevice);
         } catch (VException e) {
@@ -437,6 +437,24 @@
     }
 
     @Override
+    public void addFile(File file) {
+        try {
+            mLocalSB.files.put(mVContext, file.getId(), file, File.class);
+        } catch (VException e) {
+            handleError("Failed to add the file(" + file + "): " + e.getMessage());
+        }
+    }
+
+    @Override
+    public void deleteFile(String id) {
+        try {
+            mLocalSB.files.delete(mVContext, id);
+        } catch (VException e) {
+            handleError("Failed to delete the file with id " + id + ": " + e.getMessage());
+        }
+    }
+
+    @Override
     public void addDeviceSet(DeviceSet ds) {
         try {
             mLocalSB.deviceSets.put(mVContext, ds.getId(), ds, DeviceSet.class);
@@ -446,6 +464,15 @@
     }
 
     @Override
+    public void updateDeviceSet(DeviceSet ds) {
+        try {
+            mLocalSB.deviceSets.put(mVContext, ds.getId(), ds, DeviceSet.class);
+        } catch (VException e) {
+            handleError("Failed to update the device set(" + ds + "): " + e.getMessage());
+        }
+    }
+
+    @Override
     public void deleteDeviceSet(String id) {
         try {
             mLocalSB.deviceSets.delete(mVContext, id);
diff --git a/android/app/src/main/java/io/v/android/apps/reader/model/DeviceInfoFactory.java b/android/app/src/main/java/io/v/android/apps/reader/model/DeviceInfoFactory.java
index 1f067f7..a2c5ace 100644
--- a/android/app/src/main/java/io/v/android/apps/reader/model/DeviceInfoFactory.java
+++ b/android/app/src/main/java/io/v/android/apps/reader/model/DeviceInfoFactory.java
@@ -26,10 +26,10 @@
     /**
      * Singleton method for getting the Device object that represents this device.
      *
-     * @param context Android context
+     * @param context Android context.
      * @return Device object representing this device.
      */
-    public static Device get(Context context) {
+    public static Device getDevice(Context context) {
         Device result = instance;
         if (instance == null) {
             synchronized (DeviceInfoFactory.class) {
@@ -55,6 +55,16 @@
     }
 
     /**
+     * Convenient helper method for getting the only device id part of the device object.
+     *
+     * @param context Android context.
+     * @return device id.
+     */
+    public static String getDeviceId(Context context) {
+        return getDevice(context).getId();
+    }
+
+    /**
      * Gets the screen size.
      */
     private static Point getScreenSize(Context context) {
diff --git a/android/app/src/main/java/io/v/android/apps/reader/model/IdFactory.java b/android/app/src/main/java/io/v/android/apps/reader/model/IdFactory.java
new file mode 100644
index 0000000..bb4e098
--- /dev/null
+++ b/android/app/src/main/java/io/v/android/apps/reader/model/IdFactory.java
@@ -0,0 +1,68 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.android.apps.reader.model;
+
+import android.util.Log;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.UUID;
+
+import io.v.v23.vom.VomUtil;
+
+/**
+ * Helper class for generating id strings to be used in Syncbase tables.
+ *
+ * The purpose of this class is to make it easier to change the id generation logic,
+ * for example to make the ids consistent with the Web version Reader app.
+ */
+public class IdFactory {
+
+    private static final String TAG = IdFactory.class.getSimpleName();
+
+    /**
+     * Gets a randomly generated id string.
+     */
+    public static String getRandomId() {
+        return UUID.randomUUID().toString();
+    }
+
+    /**
+     * Gets a file id string. Uses MD5 hash to generate the key of the file.
+     * When the MD5 hashing fails, use a random id as a fallback.
+     *
+     * @param fileContents actual file contents as a byte array
+     * @return
+     */
+    public static String getFileId(byte[] fileContents) {
+        String result = getMD5HashString(fileContents);
+        if (result == null) {
+            result = getRandomId();
+        }
+
+        return result;
+    }
+
+    private static String getMD5HashString(byte[] bytes) {
+        if (bytes == null) {
+            return null;
+        }
+
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            md.update(bytes);
+            return VomUtil.bytesToHexString(md.digest());
+        } catch (NoSuchAlgorithmException e) {
+            handleException(e);
+        }
+
+        return null;
+    }
+
+    private static void handleException(Exception e) {
+        Log.e(TAG, e.getMessage(), e);
+    }
+
+}
diff --git a/android/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png b/android/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png
new file mode 100644
index 0000000..694179b
--- /dev/null
+++ b/android/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png
Binary files differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png b/android/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png
new file mode 100644
index 0000000..3856041
--- /dev/null
+++ b/android/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png
Binary files differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png b/android/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png
new file mode 100644
index 0000000..67bb598
--- /dev/null
+++ b/android/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png
Binary files differ
diff --git a/android/app/src/main/res/layout/activity_device_set_chooser.xml b/android/app/src/main/res/layout/activity_device_set_chooser.xml
index 38e1971..8c5bbb0 100644
--- a/android/app/src/main/res/layout/activity_device_set_chooser.xml
+++ b/android/app/src/main/res/layout/activity_device_set_chooser.xml
@@ -1,10 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<android.support.design.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="vertical"
     android:paddingBottom="@dimen/activity_vertical_margin"
     android:paddingLeft="@dimen/activity_horizontal_margin"
     android:paddingRight="@dimen/activity_horizontal_margin"
@@ -14,13 +15,20 @@
     <android.support.v7.widget.RecyclerView
         android:id="@+id/device_set_list"
         android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1" />
+        android:layout_height="match_parent" />
 
-    <Button
+    <android.support.design.widget.FloatingActionButton
         android:id="@+id/button_add_device_set"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:text="@string/add_device_set" />
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentEnd="true"
+        android:layout_gravity="bottom|right"
+        android:layout_margin="@dimen/fab_margin"
+        android:src="@drawable/ic_add_white_24dp"
+        app:borderWidth="0dp"
+        app:fabSize="normal"
+        app:layout_anchor="@id/device_set_list"
+        app:layout_anchorGravity="bottom|end" />
 
-</LinearLayout>
+</android.support.design.widget.CoordinatorLayout>
diff --git a/android/app/src/main/res/values-v21/styles.xml b/android/app/src/main/res/values-v21/styles.xml
deleted file mode 100644
index cc388d5..0000000
--- a/android/app/src/main/res/values-v21/styles.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<resources>
-
-    <style name="AppTheme" parent="android:Theme.Material">
-        <item name="android:colorPrimary">@color/primary</item>
-        <item name="android:colorPrimaryDark">@color/primaryDark</item>
-        <item name="android:textColorPrimary">@color/textPrimary</item>
-        <item name="android:textColor">#000000</item>
-        <item name="android:windowBackground">@color/windowBackground</item>
-        <item name="android:colorButtonNormal">#E0F2F1</item>
-    </style>
-
-</resources>
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index 67e2977..0edb971 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -4,4 +4,6 @@
     <color name="primaryDark">#00796B</color>
     <color name="textPrimary">#FFFFFF</color>
     <color name="windowBackground">#455A64</color>
+    <color name="buttonNormal">#E0F2F1</color>
+    <color name="accent">#FF9800</color>
 </resources>
diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml
index 65f95b3..be1de50 100644
--- a/android/app/src/main/res/values/dimens.xml
+++ b/android/app/src/main/res/values/dimens.xml
@@ -8,4 +8,6 @@
 
     <dimen name="device_set_list_item_horizontal_margin">16dp</dimen>
     <dimen name="device_set_list_item_vertical_margin">16dp</dimen>
+
+    <dimen name="fab_margin">16dp</dimen>
 </resources>
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index be1cf81..d043fa0 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,5 +1,4 @@
 <resources>
     <string name="app_name">PDF Reader</string>
     <string name="action_settings">Settings</string>
-    <string name="add_device_set">Add Device Set</string>
 </resources>
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
index ff6c9d2..f0720a6 100644
--- a/android/app/src/main/res/values/styles.xml
+++ b/android/app/src/main/res/values/styles.xml
@@ -1,8 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+
 <resources>
 
-    <!-- Base application theme. -->
-    <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
-        <!-- Customize your theme here. -->
+    <style name="AppTheme" parent="Theme.AppCompat">
+        <item name="android:colorPrimary">@color/primary</item>
+        <item name="android:colorPrimaryDark">@color/primaryDark</item>
+        <item name="android:textColorPrimary">@color/textPrimary</item>
+        <item name="android:textColor">#000000</item>
+        <item name="android:windowBackground">@color/windowBackground</item>
+        <item name="android:colorButtonNormal">@color/buttonNormal</item>
+        <item name="android:colorAccent">@color/accent</item>
+
+        <item name="colorPrimary">@color/primary</item>
+        <item name="colorPrimaryDark">@color/primaryDark</item>
+        <item name="colorButtonNormal">@color/buttonNormal</item>
+        <item name="colorAccent">@color/accent</item>
     </style>
 
 </resources>