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>